在 SpringBoot 项目中多数据源切换
使用dynamic-datasource-spring-boot-starter库
添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
添加配置
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
代码使用
@Service
@DS("master")
public class UserService {
@Autowired
private UserRepository userRepo;
public void queryAndUpdate(List<Integer> deptIds) {
List<User> userList = userRepo.findAll();
System.out.println(userList.size());
}
}
通过@DS注解来切换数据源,使用还是很简单的。
原理分析
- 自动配置类 DynamicDataSourceAutoConfiguration 会配置 DynamicDataSourceAnnotationInterceptor 拦截器及 DynamicRoutingDataSource 动态数据源类。
- DynamicDataSourceAnnotationInterceptor 拦截@DS注解,获取注解配置的数据源名称,如 master,存储到 DynamicDataSourceContextHolder 中,内部是一个 ThreadLocal。
- DynamicRoutingDataSource 在对象初始化时(afterPropertiesSet方法)会根据 yml 配置创建多个数据源 dataSourceMap,包含 master 和 slave,在 getConnection() 时,从 DynamicDataSourceContextHolder 中取出配置的数据源名称,根据名称从 dataSourceMap 中获取到真正的 DataSource。
自己实现一个简单的动态切换库
添加配置
和上面配置保持一致
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
定义实体类
用来接收 yml 配置
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public class MasterDataSourceProperty {
private String url;
private String username;
private String password;
private String driverClassName;
}
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
public class SlaveDataSourceProperty {
private String url;
private String username;
private String password;
private String driverClassName;
}
定义ContextHolder
public class DBContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 绑定当前线程数据源路由的key
* 使用完成后必须调用removeRouteKey()方法删除
* @param dataSource
*/
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
/**
* 获取当前线程的数据源路由的key
* @return
*/
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
/**
* 删除与当前线程绑定的数据源路由的key
*/
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
存储拦截器获取到的数据源名称,供后续使用
定义数据源配置
@Configuration
@Slf4j
public class DataSourceConfig {
@Value("${spring.datasource.dynamic.primary}")
private String defaultTargetDataSource;
/**
* 主数据源
*
* @return
*/
@Bean("masterDataSource")
public DataSource masterDataSource(MasterDataSourceProperty dataSourceProperty) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dataSourceProperty.getUrl());
dataSource.setUsername(dataSourceProperty.getUsername());
dataSource.setPassword(dataSourceProperty.getPassword());
dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
return dataSource;
}
/**
* 从数据源
*
* @return
*/
@Bean("slaveDataSource")
public DataSource slaveDataSource(SlaveDataSourceProperty dataSourceProperty) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dataSourceProperty.getUrl());
dataSource.setUsername(dataSourceProperty.getUsername());
dataSource.setPassword(dataSourceProperty.getPassword());
dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
return dataSource;
}
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
return new DynamicDataSource(defaultTargetDataSource, targetDataSources);
}
public static class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 实现数据源切换要扩展的方法,
* 通过返回值获取指定的数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDataSource();
}
public DynamicDataSource(String defaultTargetDataSource, Map<Object, Object> targetDataSources) {
//设置默认数据源
super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
//设置数据源列表
super.setTargetDataSources(targetDataSources);
}
}
}
此配置类在低版本的 SpringBoot 项目中会报错
* The dependencies of some of the beans in the application context form a cycle:
*
* masterUserService
* ↓
* userMapper defined in file [C:\D-myfiles\java\code_resp\gitee\spring_ds\target\classes\com\imooc\ds\service\UserMapper.class]
* ↓
* sqlSessionFactory defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]
* ┌─────┐
* | dynamicDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
* ↑ ↓
* | masterDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
* ↑ ↓
* | org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
* └─────┘
具体原因为:
- DynamicDataSource 在创建 Bean 时依赖 masterDataSource
- 创建 masterDataSource 的 Bean 后,会触发 DataSourceInitializerPostProcessor 处理器的 postProcessAfterInitialization()方法,此方法会创建 DataSourceInitializerInvoker 的 Bean 实例
- DataSourceInitializerInvoker 又会依赖 DataSource的 Bean 实例,这里就会依赖 DynamicDataSource(因为我们设置了@Primary注解),到这里就形成了循环依赖。
Spring 框架不支持构造器注入方式的循环依赖,但支持属性注入的循环依赖,具体原理参考 Spring源码分析之循环引用 ,所以我们的解决方案就是将构造器注入改为属性注入,具体代码见下文。
定义数据源配置(解决循环依赖)
@Configuration
@Slf4j
public class DataSourceConfig2 {
@Autowired
private MasterDataSourceProperty masterDataSourceProperty;
@Autowired
private SlaveDataSourceProperty slaveDataSourceProperty;
/**
* 主数据源
*
* @return
*/
@Bean("masterDataSource")
public DataSource masterDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
dataSource.setUsername(masterDataSourceProperty.getUsername());
dataSource.setPassword(masterDataSourceProperty.getPassword());
dataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());
return dataSource;
}
/**
* 从数据源
*
* @return
*/
@Bean("slaveDataSource")
public DataSource slaveDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
dataSource.setUsername(slaveDataSourceProperty.getUsername());
dataSource.setPassword(slaveDataSourceProperty.getPassword());
dataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());
return dataSource;
}
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource2 dataSource() {
return new DynamicDataSource2();
}
/**
* 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
*/
public static class DynamicDataSource2 extends AbstractRoutingDataSource {
@Autowired
@Qualifier("masterDataSource")
private DataSource masterDataSource;
@Autowired
@Qualifier("slaveDataSource")
private DataSource slaveDataSource;
@Value("${spring.datasource.dynamic.primary}")
private String defaultTargetDataSource;
/**
* 实现数据源切换要扩展的方法,
* 通过返回值获取指定的数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDataSource();
}
public DynamicDataSource2() {
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
//设置默认数据源
super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
//设置数据源列表
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
}
通过@Autowired注解在 Bean初始化时注入,而不是在 Bean 实例化(创建对象)时就注入,就可以解决循环依赖的问题了。或者下面这种方式也可以,还更好理解一些。
@Configuration
@Slf4j
public class DataSourceConfig3 {
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource3 dataSource() {
return new DynamicDataSource3();
}
/**
* 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
*/
public static class DynamicDataSource3 extends AbstractRoutingDataSource {
@Value("${spring.datasource.dynamic.primary}")
private String defaultTargetDataSource;
@Autowired
private MasterDataSourceProperty masterDataSourceProperty;
@Autowired
private SlaveDataSourceProperty slaveDataSourceProperty;
/**
* 实现数据源切换要扩展的方法,
* 通过返回值获取指定的数据源
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDataSource();
}
public DynamicDataSource3() {
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSources = createTargetDataSources();
//设置默认数据源
super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
//设置数据源列表
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
private Map<Object, Object> createTargetDataSources() {
HikariDataSource masterDataSource = new HikariDataSource();
masterDataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
masterDataSource.setUsername(masterDataSourceProperty.getUsername());
masterDataSource.setPassword(masterDataSourceProperty.getPassword());
masterDataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());
HikariDataSource slaveDataSource = new HikariDataSource();
slaveDataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
slaveDataSource.setUsername(slaveDataSourceProperty.getUsername());
slaveDataSource.setPassword(slaveDataSourceProperty.getPassword());
slaveDataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
return targetDataSources;
}
}
}
dynamic-datasource-spring-boot-starter 库中就是这种方式。
定义拦截器及数据源切换的注解
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value();
}
@Slf4j
@Aspect
@Component
public class DataSourceAspect {
/**
* 在类上和方法上都会拦截到
*/
@Pointcut(value = "@annotation(com.imooc.ds.custom.TargetDataSource) " +
"|| @within(com.imooc.ds.custom.TargetDataSource)")
public void dataSourcePointCut() {
}
@Before("dataSourcePointCut()")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String tragetDataSource = getTragetDataSource(method);
DBContextHolder.setDataSource(tragetDataSource);
log.info("当前线程名称:{},当前数据源:{}", Thread.currentThread().getName(), tragetDataSource);
}
private String getTragetDataSource(Method method) {
TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
if (Objects.nonNull(annotation)) {
return annotation.value();
}
Class<?> declaringClass = method.getDeclaringClass();
annotation = declaringClass.getAnnotation(TargetDataSource.class);
if (Objects.nonNull(annotation)) {
return annotation.value();
}
return null;
}
@After("dataSourcePointCut()")
public void doAfterReturning() {
DBContextHolder.clearDataSource();
}
}
拦截类或者方法上的@TargetDataSource 注解,方法优先级更高。