大家好,我是小义。在SpringBoot应用开发中,不免会遇到配置多数据源的情况,也就是需要连接多个数据库,这时我们可以引入MyBatis Plus的dynamic-datasource动态数据源插件来解决。
但是现在有这样一个场景,系统需要切换新的数据源,但是为了避免上线后新数据源可能因不稳定出问题,需要可以通过开关灵活切回旧数据源,保证系统功能正常,又该怎么实现呢?其实可以借鉴dynamic-datasource的思想。
先来看看dynamic-datasource的实现原理。在引入maven依赖后,只需要在项目配置文件设置好参数,即可通过在方法上添加@DS注解来实现动态切换。
# 配置数据源
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 注解的执行原理如下:
拦截器DynamicDataSourceAnnotationInterceptor核心代码:
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。
@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取值,这样就可以实现无需重启服务的开关效果了。
@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;
}
}
至于为啥determineCurrentLookupKey可以像threadlocal一样保证当前线程使用同一个数据源,是因为AbstrctRoutingDataSource在每次获取数据库连接时都会先调用该方法获取key,从而判断应该从Map里取哪个数据源。
//
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;
}
}
//...
至此,数据源配置已完成,以后遇到多数据源再也不怕啦。