mybatis-plus实战---多数据源@DS和@Transactional冲突
背景介绍
因为一次使用mybatis plus @DS 注解实现多数据源切换时,在一个controller中同时操作多个数据源的service方法,为了保证这一组操作下数据的完整性,则需要使用事务@Transaction进行保证,于是乎,粗暴的在controller方法上加上了该注解,原以为可以万事大吉,一运行程序发现数据源切换失败了!一泼凉水浇的是透心凉~ ,好在问题比较简单,仔细一想便知道问题出在哪里,废话不多说,咱们先看代码
代码演示
首先看一下,使用mybatis-plus 如何配置多数据源,application-local.yml配置如下
spring: datasource: dynamic: # 是否开启 SQL日志输出,生产环境建议关闭,有性能损耗 p6spy: true hikari: connection-timeout: 30000 max-lifetime: 1800000 max-pool-size: 15 min-idle: 5 connection-test-query: select 1 pool-name: OpsHikariCP # 配置默认数据源 primary: base datasource: # 数据源-1,名称为 base base: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/smart_ops?characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2b8 # 数据源-2,名称为 quartz quartz: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:/127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=GMT%2b8
需要引入maven依赖
<!-- MyBatis增强插件 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.0.0</version> </dependency> <!-- MyBatis plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <!-- MyBatis plus core --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-core</artifactId> <version>3.1.0</version> <scope>compile</scope> </dependency>
添加@DS(“quartz”)注解只用支持多数据源切换,value是你配置文件里面datasource数据源的名称即可。
1 package com.cnhqd.quartz.service; 2 3 import org.springframework.transaction.annotation.Propagation; 4 import org.springframework.transaction.annotation.Transactional; 5 import com.baomidou.dynamic.datasource.annotation.DS; 6 import com.baomidou.mybatisplus.extension.service.IService; 7 import com.cnhqd.quartz.entity.JobInfo; 8 import com.xxl.job.core.biz.model.ReturnT; 9 10 11 /** 12 * @author afj 13 * @date 2020-12-31 11:18:29 14 * (JobInfo)表服务接口 15 */ 16 @DS("quartz") 17 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) 18 public interface JobInfoService extends IService<JobInfo> { 19 20 /** 21 * 通过ID查询单条数据 22 * 23 * @param id 主键 24 * @return 实例对象 25 */ 26 JobInfo queryById(Integer id); 27 28 /** 29 * add job 30 * 31 * @param cron cron 32 * @param desc desc 33 * @param jobId job id 34 * @param status status default 0 35 * @param executorHandler handler 36 * @return ReturnT<String> 37 */ 38 ReturnT<String> createJob(String cron, String desc, String jobId, String executorHandler, final Integer status); 39 40 /** 41 * update job 42 * 43 * @param jobInfo 44 * @return 45 */ 46 ReturnT<String> updateJob(JobInfo jobInfo); 47 48 /** 49 * remove job 50 * * 51 * 52 * @param id 53 * @return 54 */ 55 ReturnT<String> remove(Integer id); 56 57 /** 58 * start job 59 * 60 * @param id 61 * @return 62 */ 63 ReturnT<String> start(Integer id); 64 65 /** 66 * stop job 67 * 68 * @param id 69 * @return 70 */ 71 ReturnT<String> stop(Integer id); 72 }
相关的业务代码如下,简单列举 JobController 中的createJob方法(这里吐槽一下,代码很low,当前版本仅为了快速实现功能)
1 /** 2 * 定时工单表(Job)控制层 3 * 下个版本计划:添加eventBus 事件监听进行应用解耦,抽取定时任务相关的依赖,形成组件 4 * 5 * @author afj 6 */ 7 @RestController 8 @RequestMapping("job") 9 @Api(tags = "定时工单相关接口") 10 @Slf4j 11 public class JobController { 12 13 private final JobService jobService; 14 private final JobConfigService jobConfigService; 15 private final JobInfoService jobInfoService; 16 17 /** 18 * Instantiates a new Meta data controller. 19 * 20 * @param jobService the meta data service. 21 * @param jobConfigService the meta data service. 22 */ 23 @Autowired(required = false) 24 public JobController(final JobService jobService, final JobConfigService jobConfigService, final JobInfoService jobInfoService) { 25 this.jobService = jobService; 26 this.jobConfigService = jobConfigService; 27 this.jobInfoService = jobInfoService; 28 } 29 30 31 /** 32 * create job. 33 * 34 * @param dto job. 35 * @return {@linkplain JsonResult} 36 */ 37 @PostMapping("add") 38 @ApiOperation(value = "新增定时工单") 39 @Transactional(rollbackFor = Exception.class) 40 @Synchronized 41 public JsonResult createJob(@RequestBody JobDto dto) { 42 if (dto.getId()!=null){ 43 JsonResult.error("参数错误"); 44 } 45 JsonResult jsonResult = jobService.saveOrUpdate(dto); 46 if (jsonResult.getCode() == ERROR_CODE) { 47 return jsonResult; 48 } 49 Job job = (Job) jsonResult.getData(); 50 JobConfig jobConfig = BeanUtil.copy(dto.getCronInfo(), JobConfig.class); 51 jobConfig.setJobId(job.getId()); 52 jobConfig.setCron(job.getCron()); 53 this.jobConfigService.add(jobConfig); 54 ReturnT<String> returnT = this.jobInfoService.createJob(job.getCron(), job.getJobName(), job.getId().toString(), ORDER_HANDLER, 0); 55 if (ReturnT.FAIL_CODE == returnT.getCode()) { 56 throw new OpsException("任务调度创建出错"); 57 } 58 job.setXxlJobId(Integer.valueOf(returnT.getContent())); 59 return jobService.update(job); 60 } 61 }
到这里就可以实现,在保证事务的同时解决多数据源切换的问题了。
分析解决
现在我们先来看一下,是怎么解决问题的。
首先,在JobController 中的createJob方法上加 @Transactional(rollbackFor = Exception.class),默认事务的传播机制是,PROPAGATION_REQUIRED ,可以不指定。
然后,在JobInfoServic 类上,添加 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) ,指定事务的传播机制是,Propagation.REQUIRES_NEW。
至此,冲突解决!
原因解析
看到这里,是不是恍然大悟。对的,就是将原本的一个事务拆分成两个事务。你可以试一下,只在JobController 中的createJob方法加事务,你会发现在切面里看数据源切换了,但事务内的数据源依然是旧的,这样就会报出XXX表找不到的问题。那么到底是什么原因导致一个事务内的数据源没有随着@DS(“quartz”)注解做出动态数据源切换呢?这里我直接说原因了,具体的解析留在下一篇来讲。
- 因为spring在开启事务的同时,会去数据库连接池拿数据库连接。如果仅在JobController 中的createJob方法上添加@Transactional,那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息(获取数据源connection连接,此时获取到的数据源是默认配置的base数据源信息)连接信息,通过 ThreadLocal 绑定在当前线程。
- 此时当前线程事务绑定的连接信息是base数据源,当我们在内层JobInfoServic使用@DS切换数据源,并没有重新开启新事务,没有改变当前线程事务的连接信息,仅仅是做了一次拦截,改变了DataSourceHolder的栈顶dataSource,对于整个事务的连接是没有影响的,所以会产生数据源没有切换的问题。
- 所以我这里的解决办法是,将保证createJob操作数据完整性的事务,拆解成两个事务,在JobInfoServic 类上,除了添加切换数据源的注解@DS(“quartz”),再添加@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class),新建开一个事务,获取新数据源connection连接。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?