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”)注解做出动态数据源切换呢?这里我直接说原因了,具体的解析留在下一篇来讲。

    1. 因为spring在开启事务的同时,会去数据库连接池拿数据库连接。如果仅在JobController 中的createJob方法上添加@Transactional,那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息(获取数据源connection连接,此时获取到的数据源是默认配置的base数据源信息)连接信息,通过 ThreadLocal 绑定在当前线程。
    2. 此时当前线程事务绑定的连接信息是base数据源,当我们在内层JobInfoServic使用@DS切换数据源,并没有重新开启新事务,没有改变当前线程事务的连接信息,仅仅是做了一次拦截,改变了DataSourceHolder的栈顶dataSource,对于整个事务的连接是没有影响的,所以会产生数据源没有切换的问题。
    3. 所以我这里的解决办法是,将保证createJob操作数据完整性的事务,拆解成两个事务,在JobInfoServic 类上,除了添加切换数据源的注解@DS(“quartz”),再添加@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class),新建开一个事务,获取新数据源connection连接。

 

转自:https://codeantenna.com/a/Ln9G3yccRs

posted @ 2022-10-19 20:52  Boblim  阅读(4192)  评论(0编辑  收藏  举报