Springboot動態切換數據源的具體實現與原理分析

前言

在springboot項目中隻需一句代碼即可實現多個數據源之間的切換:

// 切換sqlserver數據源:
DataSourceContextHolder.setDataBaseType(DataSourceEnum.SQLSERVER_DATASOURCE);
......
// 切換mysql數據源    
DataSourceContextHolder.setDataBaseType(DataSourceEnum.MYSQL_DATASOURCE);

具體實現:

本實例基於springboot2.5+版本實現。

1.配置數據源:

在配置文件中配置多個數據源的連接信息,用不同的前綴作為區別:

# sqlserver數據源1:前綴為:spring.datasource.sqlserver
spring.datasource.sqlserver.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.sqlserver.jdbc-url=jdbc:sqlserver://localhost:1433;DatabaseName=test
spring.datasource.sqlserver.username=sa
spring.datasource.sqlserver.password=sa
# mysql數據源1:前綴為:spring.datasource.mysql
spring.datasource.mysql.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.mysql.jdbc-url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true
spring.datasource.mysql.username=root
spring.datasource.mysql.password=root
# sqlLite數據源1:前綴為:spring.datasource.sqlite
spring.datasource.sqlite.driver-class-name=org.sqlite.JDBC
spring.datasource.sqlite.jdbc-url=jdbc:sqlite:D://sqllite//test.db
spring.datasource.sqlite.username=
spring.datasource.sqlite.password=
# sqlserver數據源2:前綴為:spring.datasource.sqlserver2
spring.datasource.sqlserver2.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.sqlserver2.jdbc-url=jdbc:sqlserver://localhost;DatabaseName=test1
spring.datasource.sqlserver2.username=sa
spring.datasource.sqlserver2.password=sa
# 配置數據庫連接池信息
spring.datasource.hikari.maximum-pool-size=32
spring.datasource.hikari.minimum-idle=16

2.新建枚舉類DataSourceEnum,有幾個數據源對應設置幾個枚舉類。

public enum DataSourceEnum {
    MYSQL_DATASOURCE,
    SQLSERVER_DATASOURCE,
    SQLSERVER2_DATASOURCE,
    SQLLITE_DATASOURCE
}

3.新建數據庫切換工具類DataSourceContextHolder,這裡通過ThreadLocal類型的變量來存儲當前數據源枚舉類,同時能夠保證線程安全。

public class DataSourceContextHolder {

    /**
     * 通過ThreadLocal保證線程安全
     */
    private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();

    /**
     * 設置數據源變量
     * @param dataSourceEnum 數據源變量
     */
    public static void setDataBaseType(DataSourceEnum dataSourceEnum) {
        System.out.println("修改數據源為:" + dataSourceEnum);
        contextHolder.set(dataSourceEnum);
    }

    /**
     * 獲取數據源變量
     * @return 數據源變量
     */
    public static DataSourceEnum getDataBaseType() {
        DataSourceEnum dataSourceEnum = contextHolder.get() == null ? DataSourceEnum.MYSQL_DATASOURCE : contextHolder.get();
        System.out.println("當前數據源的類型為:" + dataSourceEnum);
        return dataSourceEnum;
    }

    /**
     * 清空數據類型
     */
    public static void clearDataBaseType() {
        contextHolder.remove();
    }

4.新建DynamicDataSource類繼承AbstractRoutingDataSource類,並實現determineCurrentLookupKey方法,該方法是指定當前默認數據源的方法。

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataBaseType();
    }
}

這個類看似內容不多,但其實繼承瞭AbstractRoutingDataSource類是實現動態切換數據源的關鍵。

5.新建DataSourceConfig類用來創建bean的實例,其中包括各數據源的DataSource實例,DynamicDataSource實例以及跟Mybatis相關的SqlSessionFactory或Spring的JdbcTemplate實例。

@Configuration
public class DataSourceConfig {
    @Bean(name = "sqlserverDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlserver")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "sqlserver2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlserver2")
    public DataSource getDateSource11() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "sqlLiteDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlite")
    public DataSource getDateSource3() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("sqlserverDataSource") DataSource sqlserverDataSource,
                                        @Qualifier("sqlserver2DataSource") DataSource sqlserver2DataSource,
                                        @Qualifier("mysqlDataSource") DataSource mysqlDataSource,
                                        @Qualifier("sqlLiteDataSource") DataSource sqlLiteDataSource) {
        //配置多數據源
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceEnum.SQLSERVER_DATASOURCE, sqlserverDataSource);
        targetDataSource.put(DataSourceEnum.MYSQL_DATASOURCE, mysqlDataSource);
        targetDataSource.put(DataSourceEnum.SQLLITE_DATASOURCE, sqlLiteDataSource);
        targetDataSource.put(DataSourceEnum.SQLSERVER2_DATASOURCE, sqlserver2DataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        //多數據源
        dataSource.setTargetDataSources(targetDataSource);
        //默認數據源
        dataSource.setDefaultTargetDataSource(sqlserverDataSource);
        return dataSource;
    }
    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }

    @Bean(name = "JdbcTemplate")
    public JdbcTemplate test1JdbcTemplate(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new JdbcTemplate(dynamicDataSource);
    }
}

這樣就把我們切換數據庫鎖需要的bean全部交給spring容器中瞭,使用時直接通過DataSourceContextHolder.setDataBaseType(DataSourceEnum dataSourceEnum);這個方法指定數據源對應的枚舉類即可。

原理分析:

其實我們新建數據庫連接的時候也是通過DataSource來獲取連接的,這裡的AbstractRoutingDataSource也是通過瞭DataSource中的getConnection方法來獲取連接的。

這個類裡維護瞭兩個Map來存儲數據庫連接信息:

@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;

下面對上面的幾個屬性進行說明:

其中第一個targetDataSources是一個Map對象,在我們上面第五步創建DynamicDataSource實例的時候將多個數據源的DataSource類,放入到這個Map中去,這裡的Key是枚舉類,values就是DataSource類。

第二個defaultTargetDataSource是默認的數據源,就是DynamicDataSource中唯一重寫的方法來給這個對象賦值的。

第三個lenientFallback是一個標識,是當指定數據源不存在的時候是否采用默認數據源,默認是true,設置為false之後如果找不到指定數據源將會返回null.

第四個dataSourceLookup是用來解析指定的數據源對象為DataSource實例的。默認是JndiDataSourceLookup實例,繼承自DataSourceLookup接口。

第五個resolvedDataSources也是一個Map對象,這裡是存放指定數據源解析後的DataSource對象。

第六個resolvedDefaultDataSource是默認的解析後的DataSource數據源對象上面的getConnection方法就是從這個變量中拿到DataSource實例並獲取連接的。

總結

推薦閱讀: