完整源码: https://github.com/mucwj/example-multidatasource.git
原理:
使用注解标识要使用的数据源, 利用AOP在执行方法前切换数据源, 数据源是使用的spring提供的多数据源类(org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
)
一、多数据源实现的核心类AbstractRoutingDataSource
源码:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 省略
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*/
public void setTargetDataSources(Map
- setTargetDataSources方法设置可以切换的数据源, 参数是一个Map, 其中key是determineCurrentLookupKey方法的返回值, value是数据源实例(或者数据源名称)
- setDefaultTargetDataSource方法设置默认使用的数据源, 就是没有指定数据源的情况下使用的数据源, 参数是一个数据源实例(或者数据源名称)
-
determineCurrentLookupKey
方法是个抽象
方法, 也就是说AbstractRoutingDataSource是需要我们去继承实现的, 后面再说怎么实现, 这个方法就是返回1.
中setTargetDataSources中设置的Map的key, 通过这个key可以找到数据源
二、实现
- 假设有2个库user/order分别再mysql和postgresql中, 先写个枚举声明下, 免得后续硬编码
/**
* 数据源标签
*/
public enum DSLabel {
/**
* 用户库
*/
USER,
/**
* 订单库
*/
ORDER
}
- 注解类来标识要使用的数据源
/**
* 切换数据源的注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchDataSource {
/**
* 切换到指定的数据库
* @return 数据库标签
*/
DSLabel value();
}
- 创建一个上下文存储当前线程要使用的数据源名称(DSLabel)
/**
* 动态切换数据源的上下文, 用来修改和获取当前线程所使用的数据源标签
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DynamicDataSourceContext {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDataSourceLabel(DSLabel label) {
contextHolder.set(label);
}
public static DSLabel getDataSourceLabel() {
return contextHolder.get();
}
public static void clearDataSourceLabel() {
contextHolder.remove();
}
}
- 实现AbstractRoutingDataSource类
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContext.getDataSourceLabel();
}
}
- 利用AOP来动态切换数据源
/**
* 利用AOP来动态切换数据源
*/
@Aspect
// 保证该AOP在@Transactional之前执行
@Order(-10)
@Component
public class DynamicDataSourceAspect {
@Before("@within(SwitchDataSource) || @annotation(SwitchDataSource)")
public void changeDataSource(JoinPoint point) {
// 获取方法上的注解
Method method = ((MethodSignature)point.getSignature()).getMethod();
SwitchDataSource annotation = method.getAnnotation(SwitchDataSource.class);
DSLabel value;
if (Objects.isNull(annotation)) {
// 方法上没有注解, 获取类上的注解
annotation = point.getTarget().getClass().getAnnotation(SwitchDataSource.class);
if (Objects.isNull(annotation)) {
return;
}
}
// 获取注解值
value = annotation.value();
// 切换数据源
DynamicDataSourceContext.setDataSourceLabel(value);
}
@After("@within(SwitchDataSource) || @annotation(SwitchDataSource)")
public void clean() {
// 清理数据源的标签
DynamicDataSourceContext.clearDataSourceLabel();
}
}
- 配置
- application.yaml
multi-data-source:
data-source:
- label: USER
jdbcUrl: jdbc:mysql://localhost:3306/user?useSSL=false&allowPublicKeyRetrieval=true
username: username
password: password
- label: ORDER
jdbcUrl: jdbc:postgresql://localhost:5432/postgres?useSSL=false
username: username
password: password
- 读取配置
读取整个项目配置类
/**
* 项目配置类
*/
@Data
@ConfigurationProperties(prefix = "multi-data-source")
@Component
public class ProjectProperty {
/**
* 多个数据源配置
*/
private List datasource;
}
读取数据源配置
/**
* 数据源配置
*/
@Data
public class DataSourceProperty {
/**
* 数据源标签名称
*/
private String label;
/**
* jdbc url
*/
private String jdbcUrl;
/**
* 数据库用户名
*/
private String username;
/**
* 数据库密码
*/
private String password;
}
- 配置数据源Bean(RoutingDataSource)
@Configuration
public class DataSourceConfig {
private DataSource createDataSource(DataSourceProperty property) {
Properties properties = new Properties();
properties.setProperty("jdbcUrl", property.getJdbcUrl());
properties.setProperty("username", property.getUsername());
properties.setProperty("password", property.getPassword());
return new HikariDataSource(new HikariConfig(properties));
}
/**
* 构造RoutingDataSource数据源
* @param projectProperty 项目配置
* @return RoutingDataSource实例
*/
@Bean
public DataSource routingDataSource(ProjectProperty projectProperty) {
Map
其中routingDataSource方法中就有之前提到的2个方法setTargetDataSources、setDefaultTargetDataSource, 分别是设置所有可切换的数据源和设置默认数据源
三、使用注解切换数据源
比如说访问表tb_user的服务(UserService)是使用的mysql中的user库, 访问tb_order表的服务(OrderService)是使用的postgresql中的order库, 使用方式如下:
- 标注在类上, 相当于类中的所有方法都是使用此数据源,
注意要写在实现类上
@Service
@SwitchDataSource(DSLabel.USER)
public class UserServiceImpl implements UserService {}
@Service
@SwitchDataSource(DSLabel.ORDER)
public class OrderServiceImpl implements OrderService {}
- 标注在方法上, 标识此方法使用指定的数据源
@SwitchDataSource(DSLabel.ORDER)
public void deleteOrder(int id) {
orderMapper.deleteByUserId(id);
}
注意这种使用方式在开启事务的情况下, 如果在同一个事务下跨数据源会切换数据源失败, 原因未知... 后面再看
四、临时解决跨数据源事务问题
解决的思路就是调用跨数据源方法的时候新开一个事务, 也就是说调用方法时事务传播机制改为REQUIRES_NEW, 比如下面代码, 删除一个用户同时把用户的订单也删除, 在删除订单的方法上增加@Transactional(propagation = Propagation.REQUIRES_NEW)
即可.
虽然解决切换数据源失败的问题, 但是还有一个事务回滚的问题
, 比如说我先删除用户订单, 再删除用户, 删除用户订单成功后, 执行删除用户失败了, 这个时候删除用户订单将不会回滚, 目前只能将调用跨数据源的方法放到最后, 也就是说将删除用户订单放在删除用户后面, 后面再研究一下事务管理
@Override
public void delete(int id) {
userMapper.delete(id);
// 这里是为了能够触发AOP
((UserServiceImpl) AopContext.currentProxy()).deleteOrder(id);
}
@SwitchDataSource(DSLabel.ORDER)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteOrder(int id) {
orderMapper.deleteByUserId(id);
}