多个数据源
大家好,我是小义。在SpringBoot应用开发中,不免会遇到配置多数据源的情况,也就是需要连接多个数据库,这时我们可以引入MyBatis Plus的dynamic-datasource动态数据源插件来解决。
但是现在有这样一个场景,系统需要切换新的数据源,但是为了避免上线后新数据源可能因不稳定出问题,需要可以通过开关灵活切回旧数据源,保证系统功能正常,又该怎么实现呢?其实可以借鉴dynamic-datasource的思想。
动态数据源
先来看看dynamic-datasource的实现原理。在引入maven依赖后,只需要在项目配置文件设置好参数,即可通过在方法上添加@DS注解来实现动态切换。
代码语言:javascript复制# 配置数据源
spring:
datasource:
# 配置动态数据源信息
dynamic:
# 默认主数据源
primary: ds1
# 配置数据源信息
datasource:
# 数据源名称,自己定义
ds1:
url: jdbc:mysql://localhost:3306/test_01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
username: xxx
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
ds2:
url: jdbc:mysql://localhost:3306/test_02?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
username: xxx
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
@DS 注解的执行原理如下:
- 当方法执行时,如果该方法或其所在的类被@DS注解标记,AOP拦截器会获取注解中指定的数据源名称,并将获取到的数据源名称放入一个使用ThreadLocal管理的栈结构中,用于存储当前线程的数据源键值。这样,当前线程的所有数据库操作都会使用这个数据源。
- 在应用启动时,所有的数据源配置会被加载并存储在 DynamicRoutingDataSource 的 dataSourceMap 中,键为数据源名称,值为对应的 DataSource 实例。
- 方法执行完毕后,拦截器会从 DynamicDataSourceContextHolder 中移除当前线程的数据源键值,以确保不影响其他操作。
拦截器DynamicDataSourceAnnotationInterceptor核心代码:
代码语言:javascript复制public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
//...
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String dsKey = determineDatasourceKey(invocation);
DynamicDataSourceContextHolder.push(dsKey);
try {
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
//...
}
灵活切换
由此不难看出,数据源切换,其实就是在一个Map维护所有数据源,然后在不同条件下取不同的值。借鉴这个思路,我们再回过头来解决系统上线安全切换数据源的问题。
数据源配置
首先在配置类中定义两个数据源bean。
代码语言:javascript复制@Log4j2
@Configuration
public class DatasourceConfig {
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean(name = "ds1", initMethod = "init", destroyMethod = "close")
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
DataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
return datasource;
}
@Bean(name = "ds2")
public DataSource dataSource2() {
//...
return datasource;
}
}
动态数据源组件
借助org.springframework.jdbc.datasource.lookup.AbstrctRoutingDataSource,继承该类实现动态数据源的bean,并保存所有数据源,通过实现determineCurrentLookupKey()方法来指定具体的数据源。这里CONTEXT_HOLDER配置了apollo取值,这样就可以实现无需重启服务的开关效果了。
代码语言:javascript复制@Component
@Lazy
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
@Value("${spring.dynamicDataSource.current:ds2}")
private String CONTEXT_HOLDER;
public DynamicDataSource(@Autowired DataSource ds1, @Autowired DataSource ds2) {
HashMap<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("ds1", ds1);
targetDataSources.put("ds2", ds2);
this.setDefaultTargetDataSource(shardingSphereDataSource);
this.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER;
}
}
AbstrctRoutingDataSource
至于为啥determineCurrentLookupKey可以像threadlocal一样保证当前线程使用同一个数据源,是因为AbstrctRoutingDataSource在每次获取数据库连接时都会先调用该方法获取key,从而判断应该从Map里取哪个数据源。
代码语言:javascript复制//
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
//...
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" lookupKey "]");
} else {
return dataSource;
}
}
//...
最后
至此,数据源配置已完成,以后遇到多数据源再也不怕啦。