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;
  1. targetDataSources存储数据源列表
  2. defaultTargetDataSource是默认数据源
  3. lenientFallback 表示根据DynamicDataSource返回的lookupKey找不到数据源时是否使用默认的数据源
  4. dataSourceLookup 数据源查找,使用jndi(java命名和目录接口)的实现
  5. resolvedDataSources 将targetDataSources的value从Object转换为DataSource类型,存入resolvedDataSources
  6. 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了。

初次写博客,如有不妥之处欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值