前段时间在做会员中心和中间件系统开发时,多次碰到多数据源配置问题,主要用到分包方式、参数化切换、注解+AOP、动态添加 这四种方式。这里做一下总结,分享下使用心得以及踩过的坑。
四种方式对比
文章比较长,首先给出四种实现方式的对比,大家可以根据自身需要,选择阅读。
分包方式 | 参数化切换 | 注解方式 | 动态添加方式 | |
---|---|---|---|---|
适用场景 | 编码时便知道用哪个数据源 | 运行时才能确定用哪个数据源 | 编码时便知道用哪个数据源 | 运行时动态添加新数据源 |
切换模式 | 自动 | 手动 | 自动 | 手动 |
mapper路径 | 需要分包 | 无要求 | 无要求 | 无要求 |
增加数据源是否需要修改配置类 | 需要 | 不需要 | 不需要 | \ |
实现复杂度 | 简单 | 复杂 | 复杂 | 复杂 |
分包方式
数据源配置文件
在yml中,配置两个数据源,id分别为master和s1。
1 | yaml复制代码spring: |
数据源配置类
master数据源配置类
注意点:
需要用@Primary注解指定默认数据源,否则spring不知道哪个是主数据源;
1 | java复制代码@Configuration |
s1数据源配置类
1 | java复制代码@Configuration |
使用
可以看出,mapper接口、xml文件,需要按照不同的数据源分包。在操作数据库时,根据需要在service类中注入dao层。
特点分析
优点
实现起来简单,只需要编写数据源配置文件和配置类,mapper接口和xml文件注意分包即可。
缺点
很明显,如果后面要增加或删除数据源,不仅要修改数据源配置文件,还需要修改配置类。
例如增加一个数据源,同时还需要新写一个该数据源的配置类,同时还要考虑新建mapper接口包、xml包等,没有实现 “热插拔” 效果。
参数化切换方式
思想
参数化切换数据源,意思是,业务侧需要根据当前业务参数,动态的切换到不同的数据源。
这与分包思想不同。分包的前提是在编写代码的时候,就已经知道当前需要用哪个数据源,而参数化切换数据源需要根据业务参数决定用哪个数据源。
例如,请求参数userType值为1时,需要切换到数据源slave1;请求参数userType值为2时,需要切换到数据源slave2。
1 | java复制代码/**伪代码**/ |
设计思路
数据源注册
数据源配置类创建datasource时,从yml配置文件中读取所有数据源配置,自动创建每个数据源,并注册至bean工厂和AbstractRoutingDatasource(后面聊聊这个),同时返回默认的数据源master。
数据源切换
(1)通过线程池处理请求,每个请求独占一个线程,这样每个线程切换数据源时互不影响。
(2)根据业务参数获取应切换的数据源ID,根据ID从数据源缓存池获取数据源bean;
(3)生成当前线程数据源key;
(4)将key设置到threadLocal;
(5)将key和数据源bean放入数据源缓存池;
(6)在执行mapper方法前,spring会调用determineCurrentLookupKey方法获取key,然后根据key去数据源缓存池取出数据源,然后getConnection获取该数据源连接;
(7)使用该数据源执行数据库操作;
(8)释放当前线程数据源。
AbstractRoutingDataSource源码分析
spring为我们提供了AbstractRoutingDataSource抽象类,该类就是实现动态切换数据源的关键。
我们看下该类的类图,其实现了DataSource接口。
我们看下它的getConnection方法的逻辑,其首先调用determineTargetDataSource来获取数据源,再获取数据库连接。很容易猜想到就是这里来决定具体使用哪个数据源的。
进入到determineTargetDataSource方法,我们可以看到它先是调用determineCurrentLookupKey获取到一个lookupKey,然后根据这个key去resolvedDataSources里去找相应的数据源。
看下该类定义的几个对象,defaultTargetDataSource是默认数据源,resolvedDataSources是一个map对象,存储所有主从数据源。
所以,关键就是这个lookupKey的获取逻辑,决定了当前获取的是哪个数据源,然后执行getConnection等一系列操作。determineCurrentLookupKey是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
所以,通过扩展AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()方法,可以实现数据源的切换。
代码实现
下面贴出关键代码实现。
数据源配置文件
这里配了3个数据源,其中主数据源是MySQL,两个从数据源是sqlserver。
1 | yaml复制代码spring: |
定义动态数据源
主要是继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法。
1 | java复制代码public class DynamicDataSource extends AbstractRoutingDataSource { |
定义数据源key线程变量持有
定义一个ThreadLocal静态变量,该变量持有了线程和线程的数据源key之间的关系。当我们要切换数据源时,首先要自己生成一个key,将这个key存入threadLocal线程变量中;同时还应该从DynamicDataSource对象中的backupTargetDataSources属性中获取到数据源对象, 然后将key和数据源对象再put到backupTargetDataSources中。 这样,spring就能根据determineCurrentLookupKey方法返回的key,从backupTargetDataSources中取出我们刚刚设置的数据源对象,进行getConnection等一系列操作了。
1 | java复制代码public class DynamicDataSourceContextHolder { |
多数据源自动配置类
这里通过读取yml配置文件中所有数据源的配置,自动为每个数据源创建datasource 对象并注册至bean工厂。同时将这些数据源对象,设置到AbstractRoutingDataSource中。
通过这种方式,后面如果需要添加或修改数据源,都无需新增或修改java配置类,只需去配置中心修改yml文件即可。
1 | java复制代码@Configuration |
数据源切换工具类
切换逻辑:
(1)生成当前线程数据源key
(2)根据业务条件,获取应切换的数据源ID;
(3)根据ID从数据源缓存池中获取数据源对象,并再次添加到backupTargetDataSources缓存池中;
(4)threadLocal设置当前线程对应的数据源key;
(5)在执行数据库操作前,spring会调用determineCurrentLookupKey方法获取key,然后根据key去数据源缓存池取出数据源,然后getConnection获取该数据源连接;
(6)使用该数据源执行数据库操作;
(7)释放缓存:threadLocal清理当前线程数据源信息、数据源缓存池清理当前线程数据源key和数据源对象,目的是防止内存泄漏。
1 | java复制代码@Slf4j |
使用
1 | java复制代码public void doExecute(ReqTestParams reqTestParams){ |
每次数据源使用后,都要调用removeContextKey方法清理缓存,避免内存泄漏,这里可以考虑用AOP拦截特定方法,利用后置通知为执行方法代理执行缓存清理工作。
1 | java复制代码@Aspect |
特点分析
(1)参数化切换数据源方式,出发点和分包方式不一样,适合于在运行时才能确定用哪个数据源。
(2)需要手动执行切换数据源操作;
(3)无需分包,mapper和xml路径自由定义;
(4)增加数据源,无需修改java配置类,只需修改数据源配置文件即可。
注解方式
思想
该方式利用注解+AOP思想,为需要切换数据源的方法标记自定义注解,注解属性指定数据源ID,然后利用AOP切面拦截注解标记的方法,在方法执行前,切换至相应数据源;在方法执行结束后,切换至默认数据源。
需要注意的是,自定义切面的优先级需要高于@Transactional注解对应切面的优先级。
否则,在自定义注解和@Transactional同时使用时,@Transactional切面会优先执行,切面在调用getConnection方法时,会去调用AbstractRoutingDataSource.determineCurrentLookupKey方法,此时获取到的是默认数据源master。这时@UsingDataSource对应的切面即使再设置当前线程的数据源key,后面也不会再去调用determineCurrentLookupKey方法来切换数据源了。
设计思路
数据源注册
同上。
数据源切换
利用切面,拦截所有@UsingDataSource注解标记的方法,根据dataSourceId属性,在方法执行前,切换至相应数据源;在方法执行结束后,清理缓存并切换至默认数据源。
代码实现
数据源配置文件
同上。
定义动态数据源
同上。
定义数据源key线程变量持有
同上。
多数据源自动配置类
同上。
数据源切换工具类
切换逻辑:
(1)生成当前线程数据源key
(3)根据ID从数据源缓存池中获取数据源对象,并再次添加到backupTargetDataSources缓存池中;
(4)threadLocal设置当前线程对应的数据源key;
(5)在执行数据库操作前,spring会调用determineCurrentLookupKey方法获取key,然后根据key去数据源缓存池取出数据源,然后getConnection获取该数据源连接;
(6)使用该数据源执行数据库操作;
(7)释放缓存:threadLocal清理当前线程数据源信息、数据源缓存池清理当前线程数据源key和数据源对象。
1 | java复制代码public static void switchDataSource(String dataSourceId) { |
自定义注解
自定义注解标记当前方法所使用的数据源,默认为主数据源。
1 | java复制代码@Target(ElementType.METHOD) |
切面
主要是定义前置通知和后置通知,拦截UsingDataSource注解标记的方法,方法执行前切换数据源,方法执行后清理数据源缓存。
需要标记切面的优先级比@Transaction注解对应切面的优先级要高。否则,在自定义注解和@Transactional同时使用时,@Transactional切面会优先执行,切面在调用getConnection方法时,会去调用AbstractRoutingDataSource.determineCurrentLookupKey方法,此时获取到的是默认数据源master。这时@UsingDataSource对应的切面即使再设置当前线程的数据源key,后面也不会再去调用determineCurrentLookupKey方法来切换数据源了。
1 | java复制代码@Aspect |
使用
1 | java复制代码@UsingDataSource(dataSourceId = "slave1") |
动态添加方式(非常用)
业务场景描述
这种业务场景不是很常见,但肯定是有人遇到过的。
项目里面只配置了1个默认的数据源,而具体运行时需要动态的添加新的数据源,非已配置好的静态的多数据源。例如需要去服务器实时读取数据源配置信息(非配置在本地),然后再执行数据库操作。
这种业务场景,以上3种方式就都不适用了,因为上述的数据源都是提前在yml文件配制好的。
实现思路
除了第6步外,利用之前写好的代码就可以实现。
思路是:
(1)创建新数据源;
(2)DynamicDataSource注册新数据源;
(3)切换:设置当前线程数据源key;添加临时数据源;
(4)数据库操作(必须在另一个service实现,否则无法控制事务);
(5)清理当前线程数据源key、清理临时数据源;
(6)清理刚刚注册的数据源;
(7)此时已返回至默认数据源。
代码
代码写的比较粗陋,但是模板大概就是这样子,主要想表达实现的方式。
Service A:
1 | java复制代码public String testUsingNewDataSource(){ |
Service B:
1 | java复制代码@Transactional(rollbackFor = Exception.class) |
DynamicDataSource: 增加removeDataSource方法, 清理注册的新数据源。
1 | java复制代码public class DynamicDataSource extends AbstractRoutingDataSource { |
四种方式对比
分包方式 | 参数化切换 | 注解方式 | 动态添加方式 | |
---|---|---|---|---|
适用场景 | 编码时便知道用哪个数据源 | 运行时才能确定用哪个数据源 | 编码时便知道用哪个数据源 | 运行时动态添加新数据源 |
切换模式 | 自动 | 手动 | 自动 | 手动 |
mapper路径 | 需要分包 | 无要求 | 无要求 | 无要求 |
增加数据源是否需要修改配置类 | 需要 | 不需要 | 不需要 | \ |
实现复杂度 | 简单 | 复杂 | 复杂 | 复杂 |
事务问题
使用上述数据源配置方式,可实现单个数据源事务控制。
例如在一个service方法中,需要操作多个数据源执行CUD时,是可以实现单个数据源事务控制的。方式如下,分别将需要事务控制的方法单独抽取到另一个service,可实现单个事务方法的事务控制。
ServiceA:
1 | java复制代码public void updateMuilty(){ |
ServiceB:
1 | java复制代码@UsingDataSource(dataSourceId = "slave1") |
但是在同一个方法里控制多个数据源的事务就不是这么简单了,这就属于分布式事务的范围,可以考虑使用atomikos开源项目实现JTA分布式事务处理或者阿里的Fescar框架。
由于涉及到分布式事务控制,实现比较复杂,这里只是引出这个问题,后面抽时间把这块补上来。
参考文章
1.www.liaoxuefeng.com/article/118… Spring主从数据库的配置和动态数据源切换原理
2.blog.csdn.net/hekf2010/ar… Springcloud 多数库 多数据源整合,查询动态切换数据库
3.blog.csdn.net/tuesdayma/a… springboot-mybatis多数据源的两种整合方法
本文转载自: 掘金