mybatis-plus多数据源切换失败
一、正常使用流程
https://www.kancloud.cn/tracy5546/dynamic-datasource
特性
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密 ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 **基于seata的分布式事务方案。
- 提供 本地多数据源事务方案。
#约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线
_
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。 - 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过
spring.datasource.dynamic.primary
修改。 - 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
使用
- 引入dynamic-datasource-spring-boot-starter。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
- 配置数据源。
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
- 使用 @DS 切换数据源。
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS("dsName") | dsName可以为组名也可以为具体某个库的名称 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
二、源码调试
- 开启动态数据源的debug日志。
logging:
level:
com.baomidou.dynamic: debug
检查日志输出是否正确。
- 断点调试DynamicDataSourceAnnotationInterceptor。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
private static final String DYNAMIC_PREFIX = "#";
private final DataSourceClassResolver dataSourceClassResolver;
private final DsProcessor dsProcessor;
public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
this.dsProcessor = dsProcessor;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
//这里把获取到的数据源标识如master存入本地线程
String dsKey = determineDatasourceKey(invocation);
● DynamicDataSourceContextHolder.push(dsKey);
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
- 断点调试DynamicRoutingDataSource。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();
private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();
@Override
public DataSource determineDataSource() {
//从本地线程获取key解析最终真实的数据源
● String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}
private DataSource determinePrimaryDataSource() {
log.debug("从默认数据源中返回数据");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("从 {} 组数据源中返回数据源", ds);
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("从 {} 单数据源中返回数据源", ds);
return dataSourceMap.get(ds);
}
return determinePrimaryDataSource();
}
}
不可用版本
某些版本存在一些问题,强烈不建议使用。
强烈推荐使用最新版本。
以下版本不建议使用。
- v3.3.3 严重BUG-Advisor启动失败导致无法切换数据源 。
- v3.3.0 打包后启动强制依赖seata。
- v3.1.0 使用NamedInheritableThreadLocal导致并发高切换数据源失败。
- v2.3.3 嵌套切换数据源会导致会到主数据源。
- v2.3.2 记不清具体BUG了,不用就对了。
- v2.3.1 记不清具体BUG了,不用就对了。
- v2.2.2 记不清具体BUG了,不用就对了。
三、@DS注解失效的情况
1.开启了spring的事务。
原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。
请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional
注解。
2.方法内部调用。
查看以下示例 回答 外部调用 userservice.test1()
能在执行到 test2()
切换到second数据源吗?
public UserService {
@DS("first")
public void test1() {
// do something
test2();
}
@DS("second")
public void test2() {
// do something
}
}
答案:!!!不能不能不能!!!! 数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。
解决方法:
把test2()方法提到另外一个service,单独调用。
3.shiroAop代理。
在shiro框架中(UserRealm)使用@Autowire 注入的类, 缓存注解和事务注解都失效。
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService userService;
//... 省略其他无关的内容
}
解决方法:
1.在Shiro框架中注入Bean时, 不使用@Autowire, 使用ApplicationContextRegister.getBean()方法,手动注入bean。
2.使用@Autowire+@Lazy注解,设置注入到Shiro框架的Bean延时加载(推荐)。
4.PostConstruct初始化顺序。
初始化包括: @PostConstruct 注解, InitializingBean接口, 自定义init-method
@Component
public class MyConfiguration {
@Resource
private UserMapper userMapper;
@DS("slave")
@PostConstruct
public void init(){
// 无法选择正确的数据源
userMapper.selectById(1);
}
}
解决方法:监听容器启动完成事件, 在容器完成后做初始化。
@Component
public class MyConfiguration {
@DS("slave")
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
// 成功选择正确的数据源
userMapper.selectById(1);
}
}
相关spring源码 : `org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
在初始化完成后, bean 进入增强阶段, 所以这个阶段的任何AOP都是无效的。
5.Druid版本过低。
请升级druid1.1.22及以上版本,这个版本修复了在高并发下偶发的切换失效问题。
6.@Async或者java8的ParallelStream并行流之类方法。
这种情况都是新开了线程去处理,不受当前线程管控了。 可以在新开的方法上加对应的DS注解解决。
扩展阅读
shiro失效原因
在spring初始化bean的阶段中,大致上分为三段: BeanFactoryPostProcessor -> BeanPostProcessor -> Bean
这三种都是单例bean. 只不过会按照这个优先级进行初始化, shiro引起AOP失效的原因:
ShiroFilterFactoryBean 是一个 BeanPostProcessor , 比普通的单例Bean都优先加载, 所以他依赖注入的bean都无法正确的进行切面。
ShiroFilterFactoryBean 实际的依赖情况:
ShiroFilterFactoryBean -> SecurityManager -> UserRealm -> IUserService
IUserService依赖的其他 service 也会失效
IUserService-> MenuService -> RoleService
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端