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实例并获取连接的。