开发者

@DS+@Transactional注解切换数据源失效问题及解决

目录
  • 背景
  • 问题分析
    • 代码
    • 测试
    • 分析
  • 解决方案
    • 总结

      背景

      项目中使用了mysql数据库,并按照功能模块采用了分库的策略。因此,一个业务逻辑类中可能涉及多个MySQL数据库的操作。

      我们项目中是采用@DS(“xxx”)来实现数据源切换。

      • 当注解添加到类上,意味着此类里的方法都使用此数据源;
      • 当注解添加到方法上时,意味着此方法上使用的数据源优先级高于其他一切配置;

      问题分析

      代码

      • 依赖
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-Java</artifactId>
          <version>8.0.28</version>
      </dependency>
      <dependency>
          <groupId>com.baomidou</groupId>
          <artifactId>myBATis-plus-boot-starter</artifactId>
          <version>3.3.1</version>
      </dependency>
      <dependency>
          &landroidt;groupId>com.baomidou</groupId>
          <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
          <version>3.3.1</version>
      </dependency>
      • yml配置
      spring:
        datasource:
          dynamic:
            primary: master #设置默认的数据源或者数据源组,默认值即为master
            strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
            datasource:
              master:
                url: jdbc:mysql://localhost:3306/demo_01?useSSL=false&autoReconnect=true&characterEncoding=utf8
                username: root
                password: xxx
                driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
              slave:
                url: jdbc:mysql://172.23.168.70:3306/dynamic?useSSL=false&autoReconnect=true&characterEncoding=utf8
                username: root
                password: xxx
                driver-class-name: com.mysql.cj.jdbc.Driver
      • 对象实体
      /**
       * @author itender
       * @date 2023/4/28 11:01
       * @desc
       */
      @Data
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      @TableName("t_dynamic_template")
      public class DynamicTemplateEntity {
      
          @TableId(type = IdType.AUTO)
          private Integer id;
      
          /**
           * 语言
           */
          private String language;
      
          /**
           * 语言编码
           */
          @TableField("language_code")
          private String languageCode;
      
          /**
           * 创建时间
           */
          @TableField("created_time")
          private Date createdTime;
      
          /**
           * 创建人
           */
          @TableField("created_by")
          private Integer createdBy;
      
          /**
           * 创建人名称
         */
          @TableField("created_by_name")
          private String createdByName;
      }
      /**
       * @author itender
       * @date 2023/4/28 10:57
       * @desc
       */
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      @Builder
      @TableName("t_user")
      public class UserEntity {
      
          /**
           * 主键id
           */
          @TableId(type = IdType.AUTO)
          private Integer id;
      
          /**
           * 用户名称
           */
          private String username;
      }
      
      • controller代码
      /**
       * @author itender
       * @date 2023/4/28 10:34
       * @desc
       */
      @RestController
      @RequestMapping("template")
      public class DynamicTemplateController {
      
          private final DynamicTemplateService dynamicTemplateService;
      
          @Autowired
          public DynamicTemplateController(DynamicTemplateService dynamicTemplateService) {
              this.dynamicTemplateService = dynamicTemplateService;
          }
      
          @GetMapping
          public List<DynamicTemplateEntity> list() {
              return dynamicTemplateService.list();
          }
      
          @PostMhttp://www.devze.comapping
          public Integer add(@RequestBody DynamicTemplateEntity template) {
              return dynamicTemplateService.add(template);
          }
      }
      • service
      /**
       *php @author itender
       * @date 2023/4/28 10:36
       * @desc
       */
      public interface DynamicTemplateService {
      
          /**
           * 查询模板集合
           *
           * @return
           */
          List<DynamicTemplateEntity> list();
      
          /**
           * 添加模板
           *
           * @param template
           * @return
           */
          Integer add(DynamicTemplateEntity template);
      }
      • mapper
      /**
       * @author itender
       * @date 2023/4/28 11:09
       * @desc
       */
      @DS("slave")
      @Mapper
      @Repository
      public interface DynamicTemplateMapper extends BaseMapper<DynamicTemplateEntity> {
      }
      /**
       * @author itender
       * @date 2023/4/28 11:08
       * @desc
       */
      @Mapper
      @Repository
      @DS("master")
      public interface UserMapper extends BaseMapper<UserEntity> {
      }
      • 业务代码
      /**
       * @author itender
       * @date 2023/4/28 11:15
       * @desc
       */
      @Service
      public class DynamicTemplateServiceImpl implements DynamicTemplateService {
      
          private final DynamicTemplateMapper dynamicTemplateMapper;
      
          private final UserMapper userMapper;
      
          private final UserService userService;
      
          @Autowired
          public DynamicTemplateServiceImpl(DynamicTemplateMapper dynamicTemplateMapper, UserMapper userMapper, UserService userService) {
              this.dynamicTemplateMapper = dynamicTemplateMapper;
              this.userMapper = userMapper;
              this.userService = userService;
          }
      
          @Override
          public List<DynamicTemplateEntity> list() {
              List<DynamicTemplateEntity> templateList = dynamicTemplateMapper.selectList(new QueryWrapper<>());
              if (CollectionUtils.isEmpty(templateList)) {
                  return Lists.newArrayList();
              }
              List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
              if (CollectionUtils.isEmpty(userList)) {
                  return templateList;
              }
              Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
              templateList.forEach(template -> template.setCreatedByName(userMap.get(template.getCreatedBy())));
              return templateList;
          }
      
          @Transactional(rollbackFor = Exception.class)
          @Override
          public Integer add(DynamicTemplateEntity template) {
              List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
              if (CollectionUtiphpls.isEmpty(userList)) {
                  template.setCreatedByName("");
              }
              Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
              template.setCreatedByName(userMap.get(template.getCreatedBy()));
              template.setCreatedTime(new Date());
              dynamicTemplateMapper.insert(template);
              return template.getId();
          }
      }

      测试

      • 当方法没有@Transactional注解时,可以正常切换数据源
      [
          {
              "id": 1,
              "language": "中文",
              "languageCode": "chinese",
              "createdTime": "2023-04-27T18:56:25.000+00:00",
              "createdBy": 1,
              "createdByName": "itender"
          }
      ]

      可以正常切换数据源。

      • 当方法有@Transactional注解时,切换数据源失败
      ### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
      ### The error may exist in com/itender/threadpool/mapper/DynamicTemplateMapper.java (best guess)
      ### The error may involve com.itender.threadpool.mapper.DynamicTemplateMapper.insert-Inline
      ### The error occurred while setting parameters
      ### SQL: INSERT INTO t_dynamic_template  ( language, language_code, created_time, created_by, created_by_name )  VALUES  ( ?, ?, ?, ?, ? )
      ### Cause: java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist
      ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist] with root cause
      java.sql.SQLSyntaxErrorException: Table 'demo_01.t_dynamic_template' doesn't exist

      分析

      • spring 的@Transactional声明式事务管理时通过动态代理实现的。
      • @DS注解加到mapper接口、service接口、service方法里都不生效,获取的还是默认的主数据源。猜测是由于spring的aop切面机制导致拦截不到@DS注解,进而不能切换数据源,正确的做法是添加到service实现类或者实现类里具体的方法上。
      • 在事务方法内调用@DS注解的方法,@DS注解同样不生效,原因是spring只能拦截到最外层方法的@Transactional注解,此时加载该事务的数据源,在事务方法内即使调用了@DS注解的方法,获取的是外层事务的数据源,导致@DS失效。
      • 在同一个实现类中,一个非DS注解的常规方法里调用@DS注解的方法,同样存在@DS失效的情况,原因同2,是由spring的aop机制导致的,如果确有这种业务需要,可以将该DS注解方法定义在不同的类中,通过bean注入的方式调用,就不会出现这个问题。

      解决方案

      • 把查询user的逻辑放到另外一个单独的业务逻辑类里面
      /**
       * @author itender
       * @date 2023/4/28 14:25
       * @desc
       */
      public interface UserService {
      
          /**
           * 查询用户集合
           *
           * @return
           */
          List<UserEntity> list();
      }
      /**
       * @author itender
       * @date 2023/4/28 14:27
       * @desc
       */
      @Service
      public class UserServiceImpl implements UserService {
      
          private final UserMapper userMapper;
      
          @Autowired
          public UserServiceImpl(UserMapper userMapper) {
              this.userMapper = userMapper;
          }
      
          @DS("master")
          @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
          @Override
          public List<UserEntity> list() {
              return userMapper.selectList(new QueryWrapper<>());
          }
      }
      • 修改template业务类
      @DS("slave")
      @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
      @Override
      public Integer add(DynamicTemplateEntity template) {
          // List<UserEntity> userList = userMapper.selectList(new QueryWrapper<>());
          List<UserEntity> userList = userService.list();
          if (CollectionUtils.isEmpty(userList)) {
              template.setCreatedByName("");
          }
          Map<Integer, String> userMap = userList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getUsername, (key1, key2) -> key1));
          template.setCreatedByName(userMap.get(template.getCreatedBy()));
          template.setCreatedTime(new Date());
          dynamicTemplateMapper.insert(template);
          return template.getId();
      }

      测试成功插入一条数据。

      总结

      spring 的@Transactional声明式事务管理时通过动态代理实现的。

      @DS注解加到mapper接口、service接口、service方法里都不生效,获取的还是默认的主数据源。猜测是由于spring的aop切面机制导致拦截不到@DS注解,进而不能切换数据源,正确的做法是添加到service实现类或者实现类里具体的方法上。

      在事务方法内调用@DS注解的方法,@DS注解同样不生效,原因是spring只能拦截到最外层方法的@Transactional注解,此时加载该事务的数据源,在事务方法内即使调用了@DS注解的方法,获取的是外层事务的数据源,导致@DS失效。

      php

      在同一个实现类中,一个非DS注解的常规方法里调用@DS注解的方法,同样存在@DS失效的情况,原因同2,是由spring的aop机制导致的,如果确有这种业务需要,可以将该DS注解方法定义在不同的类中,通过bean注入的方式调用,就不会出现这个问题。

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜