浅谈使用Spring多数据源路由AbstractRoutingDatasource实现动态切换数据源

2022-06-26 09:16:28

为了帮助理解此原理,在SpringBoot 2.1.17版本上,借助一个具体的功能来展开讨论(这个功能就是通过页面配置数据源基本信息,并进行测试连接)。

前端页面配置数据源名称(唯一标识)、类型(mysql/oracl 等)、ip、端口、数据库名称、用户名、密码。后台根据配置信息生成创建com.alibaba.druid.pool.DruidDataSource 的必要信息。

1.创建 DynamicDataSource  集成AbstractRoutingDataSource


/**
 * 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 当前已加载的数据源
     */
    private Map<Object, Object> currentDataSources = new HashMap<>(8);
    private DruidProperties druidProperties;
    //=====================================分析1
    public DynamicDataSource(DataSource defaultTargetDataSource, 
Map<Object, Object> targetDataSources, DruidProperties druidProperties) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        setCurrentDataSources(targetDataSources);
        super.afterPropertiesSet();
        this.druidProperties = druidProperties;
    }
    private void setCurrentDataSources(Map<Object, Object> targetDataSources) {
        this.currentDataSources = targetDataSources;
    }
    public Map<Object, Object> getCurrentDataSources() {
        return this.currentDataSources;
    }
    /**
     * 重置数据源
     */
    private void resetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
    public void addDataSource(String username, String password, 
String url, String driverClassName, String dsKey) {
        if(! currentDataSources.containsKey(dsKey)){//不存在则put
            DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            dataSource.setUrl(url);
            dataSource.setDriverClassName(driverClassName);
            dataSource = druidProperties.dataSource(dataSource);
            currentDataSources.put(dsKey, dataSource);
            resetDataSources(currentDataSources);
        }
    }
}

2.创建 DynamicDataSourceContextHolder 操作线程ThreadLocal。保存数据源名称(唯一标识)

/**
 * 数据源切换处理
 * 
 */
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }
    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

3.创建DruidConfig 处理初始化时主数据源的加载。



/**
 * druid 配置多数据源
 */
@Configuration
public class DruidConfig
{
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", 
name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource, 
DruidProperties druidProperties)
    {
        Map<Object, Object> targetDataSources = new HashMap<>(4);
        targetDataSources.put("MASTER", masterDataSource);
        setDataSource(targetDataSources, "SLAVE", "slaveDataSource");
        return new DynamicDataSource(masterDataSource,
 targetDataSources, druidProperties);
    }
    /**
     * 设置数据源
     */
    public void setDataSource(Map<Object, Object> targetDataSources,
 String sourceName, String beanName)
    {
        try
        {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e)
        {
        }
    }

}
 分析1:服务启动初始化,加载配置DruidConfig 创建dynamicDataSource ,将application-druid.yml中的主数据源(默认数据)设置到AbstractRoutingDataSource的 默认数据源Object defaultTargetDataSource。
 将主数据与和从数据源(如存在)保存到已加载的数据源 Map<Object, Object> targetDataSources中,Map的key为数据源名称(唯一)。执行afterPropertiesSet(),
将默认主数据源赋值resolvedDefaultDataSource,已加载的数据源设置到resolvedDataSources。
因此当服务做查询操作时,调用getConnection()执行determineTargetDataSource()方法,因为DynamicDataSource重写了AbstractRoutingDataSource 的determineCurrentLookupKey方法,去读取当前线程的ThreadLocal,获取不到,返回resolvedDefaultDataSource即主数据源,从而查询操作的是主数据库。
public abstract class AbstractRoutingDataSource extends AbstractDataSource 
implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources; //已加载的数据源
    @Nullable
    private Object defaultTargetDataSource;//默认数据源
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;//已经加载的数据源
    @Nullable
    private DataSource resolvedDefaultDataSource;//默认数据源

    
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("xx");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource= this.resolveSpecifiedDataSource
(this.defaultTargetDataSource);
            }

        }
    }

 @Nullable
    protected abstract Object determineCurrentLookupKey();

  public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }


    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) {
          //xx
        } else {
            return dataSource;
        }
    }

}

4.页面添加数据源并测试连接。创建控制层.

     //=========================分析2    
    @PostMapping("/testConneciton")
    @ResponseBody
    public AjaxResult testConnection(MyDatasourceTable myDatasourceTable) {

        putDataSource(myDatasourceTable);
        DynamicDataSourceContextHolder.setDataSourceType(myDatasourceTable.getDatasourceName());
        try {
            int reult = serviceImpl.selectxxx();//做数据库查询
           
            if (reult > 0) {
                return AjaxResult.success("连接成功");

            } else {
                return AjaxResult.warn("连接失败");
            }

        } catch (Exception e) {
            return AjaxResult.error("连接失败或用户名密码错误");
        }finally{
             DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    private void putDataSource(MyDatasourceTable myDatasourceTable) {
        String datasourceUrl = "";
        if ("mysql".equals(myDatasourceTable.getDatasourceType())) {
            myDatasourceTable.setDatasourceDrive("com.mysql.cj.jdbc.Driver");
            datasourceUrl = "jdbc:mysql://" + 
myDatasourceTable.getDatasourceIp() + ":" + String.valueOf(myDatasourceTable.getDatasourcePort().intValue()) + "/" +
 myDatasourceTable.getDatasourceInstantname() + "?" + "useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true";
        } 
        myDatasourceTable.setDatasourceUrl(datasourceUrl);
        String username = myDatasourceTable.getDatasourceUsername();//数据库用户名
        String password = myDatasourceTable.getDatasourcePassword();//密码
        String url = datasourceUrl;//连接地址
        String driverClassName = myDatasourceTable.getDatasourceDrive();//驱动名称
        String dsType = myDatasourceTable.getDatasourceName();//数据名称
        dynamicDataSource.addDataSource(username, password, 
        url, driverClassName, dsType);
    }

分析2 :

putDataSource方法,将新增数据源保存到加载的数据源targetDataSources中,同时重新设置 resolvedDataSources。

DynamicDataSourceContextHolder.setDataSourceType(myDatasourceTable.getDatasourceName());此方法给当前线程的ThreadLocal中保存新增的数据源名称A(唯一标识),可以理解成切换了数据源。在执行查询数据源操作时,要获取连接,调用AbstractRoutingDataSource 路由的getConnection()执行determineTargetDataSource()方法,进一步的this.determineCurrentLookupKey()返回当前线程ThreadLocal保存的数据源名称,所以resolvedDataSources返回了新增数据源A,从而查询操作的是数据源A。查询操作完成后,  DynamicDataSourceContextHolder.clearDataSourceType(); 清除ThreadLocal保存的数据源名称A(唯一标识)。

当业务再次触发数据库查询操作的时候,就回到分析1中,获取不到当前线程的ThreadLocal即返回主数据源,操作主数据源。

总结:

1.DynamicDataSourceContextHolder.setDataSourceType切换数据源,

DynamicDataSourceContextHolder.clearDataSourceType()清除,这两个方法需要搭配使用,将清除ThreadLocal数据放入fianlly中执行,确保本次切换成功切回。

2.使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

愚见,持续优化更新中。

  • 作者:那个新生代民工
  • 原文链接:https://blog.csdn.net/qq_17215887/article/details/124167029
    更新时间:2022-06-26 09:16:28