Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 切换数据源失败总结
文章目录
- [Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 切换数据源失败总结](#Springboot+mybatis-plus+dynamic-datasource+Druid 多数据源 切换数据源失败总结)
- 0.前言
- [1. dynamic-datasource 切换数据源失败场景总结](#1. dynamic-datasource 切换数据源失败场景总结)
-
- [1. spring-batch整合情况下切换数据源异常](#1. spring-batch整合情况下切换数据源异常)
- 解决办法:
- [2. 使用了Spring 原生事务注解导致多数据源切换失败](#2. 使用了Spring 原生事务注解导致多数据源切换失败)
- [3. 内部方法调用导致的](#3. 内部方法调用导致的)
- [4 . Shiro框架问题](#4 . Shiro框架问题)
- [5. PostConstruct初始化顺序](#5. PostConstruct初始化顺序)
- [6. Druid版本太低](#6. Druid版本太低)
- [7. 新开线程导致(@Async或者java8的ParallelStream并行流之类方法。)](#7. 新开线程导致(@Async或者java8的ParallelStream并行流之类方法。))
- [3. 参考资料](#3. 参考资料)
0.前言
背景 dynamic-datasource 是苞米豆(baomidou) 团队中@小锅盖开源的一款很优秀的多数据源管理组件,特别方便也很强大,在spring boot中简直就是开箱即用。但是也有很多小问题,主要是在多数据源切换失效这块。我们公司也是重依赖dynamic-datasource
.也修复了一些问题。特此整理总结一下。首先我们拉出来一些issue 大家看下。
- 支持子线程继承主线程的数据源code https://github.com/baomidou/dynamic-datasource/issues/502
- 加上@DS 用的是postgrel数据库,数据源切换失败 https://github.com/baomidou/dynamic-datasource/issues/385
- 《多层数据源嵌套切换,开启事务的情况下无效》https://github.com/baomidou/dynamic-datasource/issues/248
- 关于 子线程数据源切换失败的原因,也有一些同学在github Issues 里给出了自己项目的最佳实践,不过作者考虑到该组件的通用性,以及异步场景较少的情况,给出的答复是,"用异步的少,不想支持."。此同学还在继续说教以及列举事实,说不定,@小锅盖会在下个版本中将此缺陷完美修复,大家敬请期待
1. dynamic-datasource 切换数据源失败场景总结
1. spring-batch整合情况下切换数据源异常
github issue 地址 https://github.com/baomidou/dynamic-datasource/issues/340
解决办法:
作者给的办法, 读的时候不用事务或者单独事务,写的时候单独开事务
2. 使用了Spring 原生事务注解导致多数据源切换失败
解决办法
可能需要查看调用链路上涉及的类和方法,看看是否有@Transactional注解。如果在必要的情况下需要保证多个数据库的事务一致性,你需要采用分布式事务的解决方法,seate 作者已经在mybatis-plus提交了一个PR 解决了这个问题,只需要使用@DSTransactional即可。
原理解析:
在Spring项目中,Spring提供了事务管理的功能,主要通过@Transactional这个注解实现。在被@Transactional注解的方法中,Spring会维护一个ConnectionHolder,它含有当前事务所使用的数据库连接,这就保证了在这个事务中,所有的数据库操作都在一个数据库连接中完成,也就保证了事务的原子性。
但这时如果你使用了dynamic-datasource进行数据源切换,就会出现问题。因为dynamic-datasource是通过AOP的方式,在调用方法前切换数据源的。但在@Transactional注解的方法中,Spring已经从某一个数据源获取了连接,而这个连接在整个事务中是不会变的。所以,当你在这个事务中的某个地方希望切换到另一个数据源时,dynamic-datasource虽然可以切换数据源,但已经无法改变Spring事务已经获得的那个数据库连接,导致实际的数据库操作还是在原来的数据库中进行。
解决思路
- 让需要切换数据源的操作不在事务中进行,如无必要不加事务原则。
- 如果必须要在事务中进行,那么你可能需要查看调用链路上涉及的类和方法,看看是否有@Transactional注解。如果在必要的情况下需要保证多个数据库的事务一致性,你需要采用分布式事务的解决方法或者使用本地多数据源事务注解@DSTransactional这个是seata的作者写的还是很靠谱的 。
- 如果不想
@DSTransactional
,可以自己使用JTA(Java Transaction API)全局事务实现框架替代《Spring Boot+Atomikos进行多数据源的分布式事务管理详解和实例》,在多个数据源之间进行事务管理。这需要相应的XA驱动的支持。但它会带来一定的性能开销,适合对数据一致性要求较高的场景,并且要有一定的技术功底,如果想图省事,就使用作者提供的@DSTransactional
注解。
3. 内部方法调用导致的
这个所有基于AOP实现的功能都存在这个问题。所以在当前方法内调用。数据源切换是不会被触发的。此处不做详细解读,可聊一下动态代理条件和原理即可明白、
在Spring中,数据源切换的实现原理是基于AOP代理的。故而,如果你在方法内部调用另一个需要切换数据源的方法,
解决办法
将需要切换数据源的方法提取到另一个service中,然后在外部单独调用。
4 . Shiro框架问题
这个是官方文档中提出的,我们项目没有遇到。
在使用Shiro框架中,如果你使用@Autowired注入的类,可能会发现事务注解和缓存注解失效问题。
原理解析
在Spring框架中,对Bean实例化、初始化的过程会涉及到几个关键的拦截点,包括BeanFactoryPostProcessor、BeanPostProcessor等。在这个过程中,这些拦截点的加载顺序有其严格的优先级:
BeanFactoryPostProcessor:最早加载的,它会在BeanDefinition加载完成后及Instantiation(实例化)前进行拦截,这个时候所有的Bean定义信息已经加载到Spring中,但所有的Bean都还未被实例化。
BeanPostProcessor:相对BeanFactoryPostProcessor来说,它的加载时机稍晚,它会在Bean的生命周期的初始化阶段和销毁阶段进行拦截。
Bean:普通的Bean的加载顺序在上述两者之后。
产生问题的关键就是其中的加载顺序。由于Shiro的ShiroFilterFactoryBean是BeanPostProcessor,所以它会比普通的Bean(如UserController,UserService等)要早加载。由于AOP就是通过BeanPostProcessor来实现的,若ShiroFilterFactoryBean已经在AOP处理之前被加载,那么在ShiroFilterFactoryBean中注入的Bean就无法被AOP进行增强(如@Transactional注解的事务增强),所以引发了依赖注入的Bean失效问题。
例如,在以下的依赖链中:
ShiroFilterFactoryBean -> SecurityManager -> UserRealm -> IUserService
IUserService下游依赖的其他 service 例如 MenuService、RoleService 等,都会由于上述原因,导致无法正常工作,用户在这些Service中使用诸如@Transactional等注解,将不会起到预期的效果。
解决办法
- 手动获取Bean方法,可以通过ApplicationContext.getBean()手动获取;
- 使用@Lazy注解,让Bean的初始化推迟,待AOP处理完成后再实例化Bean。
java
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService userService;
//... 省略其他无关的内容
}
5. PostConstruct初始化顺序
此处直接照搬官方给的文档
这是Spring框架的特性,涉及到Spring的生命周期和Bean初始化过程。
Spring容器创建Bean对象的过程中,会按照以下顺序进行:
1.实例化:使用反射机制,根据配置文件创建对应的Bean对象。
2.属性赋值:根据配置文件中,使用set方法进行属性赋值。
3.Bean后置处理器Before:在Bean对象初始化前进行一些处理。
4.初始化:对Bean进行一些自定义的初始化。
对象实现了InitializingBean接口,会执行afterPropertiesSet方法。
对象有配置@PostConstruct注解的方法,会执行该方法。
在配置文件中通过init-method指定的初始化方法。
5. Bean后置处理器After:在Bean对象初始化后进行一些处理。
通过以上的顺序可以看出,当执行用户设定的初始化(包括@PostConstruct,afterPropertiesSet,init-method)时,Bean后置处理器(包括AOP)还未执行,因此在这个阶段是无法获取到AOP增强后的代理对象的。
初始化包括:PostConstruct注解,InitializingBean接口,自定义init-method。在这个阶段,任何AOP都无效。
java
@Component
public class MyConfiguration {
@Resource
private UserMapper userMapper;
@DS("slave")
@PostConstruct
public void init(){
// 无法选择正确的数据源
userMapper.selectById(1);
}
}
解决方法:
监听容器启动完成事件, 在容器完成后做初始化。
java
@Component
public class MyConfiguration {
@DS("slave")
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
// 成功选择正确的数据源
userMapper.selectById(1);
}
}
相关spring源码 : `org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
6. Druid版本太低
如果你的Druid版本过低,可能会在高并发下出现数据源切换失效的问题。
解决办法:更新你的Druid版本到1.1.22及以上版本。
7. 新开线程导致(@Async或者java8的ParallelStream并行流之类方法。)
在使用@Async或者java8的ParallelStream等需要新开线程的场景下,数据源切换同样会出现失效问题。
在处理并发任务时,Spring的@Async注解或者Java 8的ParallelStream是我们常用的工具。它们可以帮助我们在新的线程中处理任务,提高程序的运行效率。然而,由于这些新的线程是独立于原来的线程的,原来线程中的数据源切换并不能传递到新的线程中,这就可能导致在新的线程中对数据库的操作还是使用的原来线程的数据源,不能正确地进行数据源切换。
解决办法
在新开的方法上添加对应的DS注解。可以通过在新的方法上添加对应的DS注解来解决这个问题。DS注解可以指定这个方法使用的数据源,这样即使这个方法在新的线程中被执行,它还是会使用我们通过DS注解指定的数据源。
例如:
java
@DS("second")
@Async
public void asyncMethod() {
// 这个方法将在新的线程中执行,并使用"second"数据源
}
同样,对于java8的ParallelStream并行流执行的方法,我们也可以在方法上添加DS注解来指定数据源:
java
@DS("second")
public void parallelMethod() {
List<String> data = ...;
data.parallelStream().forEach(item -> {
asyncMethod() // 这个方法将在新的线程中执行,并使用"second"数据源
});
}
3. 参考资料
- dynamic-datasource GitHub 仓库 ↗:dynamic-datasource 的官方 GitHub 仓库,包含源代码、文档和示例等资源。