SpringBoot动态数据源,还能玩出新花样,又学废了!

2024-09-24 18:36:14 浏览数 (1)

多个数据源

大家好,我是小义。在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 注解的执行原理如下:

  1. 当方法执行时,如果该方法或其所在的类被@DS注解标记,AOP拦截器会获取注解中指定的数据源名称,并将获取到的数据源名称放入一个使用ThreadLocal管理的栈结构中,用于存储当前线程的数据源键值。这样,当前线程的所有数据库操作都会使用这个数据源。
  2. 在应用启动时,所有的数据源配置会被加载并存储在 DynamicRoutingDataSource 的 dataSourceMap 中,键为数据源名称,值为对应的 DataSource 实例。
  3. 方法执行完毕后,拦截器会从 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;
        }
    }
    //...

最后

至此,数据源配置已完成,以后遇到多数据源再也不怕啦。

0 人点赞