springboot基于AbstractRoutingDataSource配置双数据源及源码解析
问题描述
我们使用springboot开发的项目一般是单数据源的,这个时候只需在配置文件中指明数据源的配置信息,springboot会自动为我们创建DataSource、TransactionManager等数据库相关操作对象。但是在中大型项目中为了减少单数据库的压力、实现读写分离等需要使用2个或2个以上的数据源,这个时候该怎么去配置呢?
解决方法
AbstractRoutingDataSource类是spring为我们提供的轻量级数据源切换方式,它是一个抽象类,继承自AbstractDataSource,实现了DataSource接口(javax.sql)。基本思想是程序在运行时通过AbstractRoutingDataSource动态的将用户定义的数据源动态地织入到程序中,实现数据源的切换。这里我使用到的技术或类有ThreadLocal、aop、自定义注解等。
具体实现
application.properties数据源配置
这里我配置了2个数据源,一个是oracle,另一个是mysql,其他配置如tomcat端口号等在这里省略,没有列出
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# oracle
spring.datasource.oracle.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.oracle.url=jdbc:oracle:thin:@localhost:1521:test
spring.datasource.oracle.username=root
spring.datasource.oracle.password=123
# mysql
spring.datasource.mysql.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.mysql.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
spring.datasource.mysql.username=root
spring.datasource.mysql.password=123
定义枚举类DataSourceType和自定义注解@DataSource
DataSourceType用于定义数据源的类别
public enum DataSourceType {
MYSQL,
ORACLE
}
@DataSource用于标记在controller或service的方法上,并给定参数指出该方法使用的数据源。
ps: @DataSource和javax.sql的DataSource接口同名了,但是并不影响我们使用该注解,之前我用的是@DS这个名字,后来觉得只看缩写不知道是什么,就改成了DataSource,觉得同名不好的小伙伴可以改成其他的名字。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType value() default DataSourceType.MYSQL;
}
定义DataSourceContextHolder
当我们在进行增删改查的时候,对于每一个线程应该有对应的数据源,所以使用ThreadLocal来存储当前线程对应的数据源,ThreadLocal可以定义在后面介绍的DynamicDataSource类里,也可以定义在其他类中,比如这里定义在DataSourceContextHolder类中。
public class DataSourceContextHolder {
//为每个线程存储对应的数据源
private static ThreadLocal<DataSourceType> context=new ThreadLocal<>();
public static void setDataSource(DataSourceType datasource){
context.set(datasource);
}
public static DataSourceType getDataSource(){
return context.get();
}
//移除存储的数据源信息
public static void clearDataSource(){
context.remove();
}
}
定义DynamicDataSource
该类继承自AbstractRoutingDataSource,我们需要实现determineCurrentLookupKey()方法,返回给spring一个代表数据源的key,通过该key,在AbstractRoutingDataSource的resolvedDataSources(Map<Object, DataSource>类型)中查找到对应的数据源datasource。
public class DynamicDataSource extends AbstractRoutingDataSource {
//根据DataSourceContextHolder中的值获取数据源的key
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
定义DynamicDataSourceAspect
该类是为了使用自定义注解@DataSource使用的,在@DataSource标记的方法前切换为@DataSource参数指定数据源,方法后清除DataSourceContextHolder中的数据源信息,以免影响默认的数据源。
@Component
@Aspect
public class DynamicDataSourceAspect {
@Around("@annotation(com.zyl.datasource.DataSource)")
public void switchDS(JoinPoint p) {
//获得注解所在方法签名
Method m=((MethodSignature)p.getSignature()).getMethod();
DataSource annotation = m.getAnnotation(DataSource.class);
DataSourceType datasource=annotation.value();
DataSourceContextHolder.setDataSource(datasource);//修改所用数据源为注解参数
try {
((ProceedingJoinPoint)p).proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
DataSourceContextHolder.clearDataSource();//清除数据源信息
}
}
}
基于druid的数据源配置DruidDataSourceConfig
这里我是用的数据库连接池是druid,当然你也可以使用其他的数据库连接池。使用java config的方式配置动态的数据源。
@Configuration
public class DruidDataSourceConfig {
//配置oracle、mysql数据源
@Bean("mysql")
@ConfigurationProperties("spring.datasource.mysql")
public DataSource dataSource1(){
return DruidDataSourceBuilder.create().build();
}
@Bean("oracle")
@ConfigurationProperties("spring.datasource.oracle")
public DataSource dataSource2(){
return DruidDataSourceBuilder.create().build();
}
@Primary //当相同类型的实现类存在时,选择该注解标记的类
@Bean("dynamicDS")
public DataSource dataSource(){
DynamicDataSource dds=new DynamicDataSource();
//默认数据源,mysql
dds.setDefaultTargetDataSource(dataSource1());
Map<Object,Object> dsMap=new HashMap<>();
dsMap.put(DataSourceType.MYSQL,dataSource1());
dsMap.put(DataSourceType.ORACLE,dataSource2());
dds.setTargetDataSources(dsMap);
return dds;
}
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
}
使用方式
AbstractRoutingDataSource 是只支持单库事务的,也就是说切换数据源要在开启事务之前执行,事务开启后不能再切换数据源。在一个方法上同时标记
@Transactional和@DataSource会导致切换数据源失败,解决方法是将切换数据源和开启事务放在mvc的不同层次中。
1.在controller或service的方法上加@DataSource(DataSourceType.MYSQL)使整个方法使用mysql
2.在controller或service的方法中直接使用DataSourceContextHolder.setDataSource(DataSourceType.MYSQL)指定连接mysql
源码解读
AbstractRoutingDataSource
AbstractRoutingDataSource类有如下属性:
@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存储数据源列表
- defaultTargetDataSource是默认数据源
- lenientFallback 表示根据DynamicDataSource返回的lookupKey找不到数据源时是否使用默认的数据源
- dataSourceLookup 数据源查找,使用jndi(java命名和目录接口)的实现
- resolvedDataSources 将targetDataSources的value从Object转换为DataSource类型,存入resolvedDataSources
- resolvedDefaultDataSource 将defaultTargetDataSource转化为DataSource类型,存入resolvedDefaultDataSource
AbstractRoutingDataSource类的核心方法有2个,分别是afterPropertiesSet()和determineTargetDataSource()。
我们来回顾一下在DruidDataSourceConfig中dataSource()方法创建的动态数据源
@Primary //当相同类型的实现类存在时,选择该注解标记的类
@Bean("dynamicDS")
public DataSource dataSource(){
DynamicDataSource dds=new DynamicDataSource();
//默认数据源,mysql
dds.setDefaultTargetDataSource(dataSource1());
Map<Object,Object> dsMap=new HashMap<>();
dsMap.put(DataSourceType.MYSQL,dataSource1());
dsMap.put(DataSourceType.ORACLE,dataSource2());
dds.setTargetDataSources(dsMap);
return dds;
}
该方法返回的是DynamicDataSource类型的数据源,不要忘记DynamicDataSource是继承自AbstractRoutingDataSource的。首先实例化了一个DynamicDataSource,设置了默认的数据源,放入defaultTargetDataSource中,然后将数据源列表mysql、oracle以Map的形式放入了targetDataSources中。
afterPropertiesSet()、determineTargetDataSource()、这里创建数据源的dataSource()方法的调用顺序是:spring启动–>dataSource()–>afterPropertiesSet();当我们设置数据源DataSourceContextHolder.setDataSource()的时候会调用determineTargetDataSource()
接下来看看afterPropertiesSet()做了些什么:
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
targetDataSources为空抛异常,遍历targetDataSources,这里使用了forEach方法,forEach方法内部其实是个foreach的循环,resolveSpecifiedLookupKey(key)其实直接返回了key,resolveSpecifiedDataSource(value)是将targetDataSources的value(Object类型)转换为DataSource类型,放入resolvedDataSources这个Map中。
可以看看resolveSpecifiedDataSource(value)的实现:
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
}
else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String) dataSource);
}
else {
throw new IllegalArgumentException(
"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}
如果dataSource是DataSource类型直接返回,如果是String类型,用JndiDataSourceLookup去查找dataSource,否则抛个不合法参数异常。总之就是将Object类型转换为DataSource类型。
至此afterPropertiesSet()执行完毕,resolvedDefaultDataSource存储了默认数据源,resolvedDataSources存放了可用数据源列表。
当我们设置数据源DataSourceContextHolder.setDataSource()的时候会调用determineTargetDataSource(),看看determineTargetDataSource()又做了什么:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
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 + "]");
}
return dataSource;
}
可以看到调用了我们的DynamicDataSource重写的determineCurrentLookupKey()方法,返回的正是我们在DataSourceContextHolder设置的dataSource的key,然后根据这个key在resolvedDataSources里查找DataSource ,如果找到直接返回,没找到并且lenientFallback为true(默认为true)或lookupKey为null,就会使用默认的数据源resolvedDefaultDataSource,lenientFallback前面已经说过表示DynamicDataSource返回的lookupKey找不到数据源时是否使用默认的数据源。如果上述条件都不满足,抛异常。
这个determineTargetDataSource()调用完了,数据源也就设置成我们在DataSourceContextHolder中设置的dataSource了。
初次写博客,如有不妥之处欢迎指正。