Mybatis plus的多数据源@DS切换不起作用了,谁的锅

Mybatis plus的多数据源@DS切换不起作用了,谁的锅

由于使用了微服务,会有多个数据库的情况,有时业务需要,需要切换数据源,所以使用了 Mybatis plus 的@DS 来切换多数据源

yml 数据库配置如下:

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://user
          username: root
          password: root
        common:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://book
          username: root
          password: root

service 如下,默认是 master 数据源

@Service
@Slf4j
public class MasterService {
    @Autowired
    UserService userService;
    @Autowired
    BookService bookService;

    /**必须master库方法先执行,才能回滚,达到事务效果*/
    @Transactional(rollbackFor = Exception.class)
    public void upload(ReqDto reqDto){
        userService.save(reqDto);
        bookService.save(reqDto);
    }
}

userService

@Service
@Log4j2
public class UserService extends ServiceImpl<UserMapper, User> {
    @Resource
    private UserMapper userMapper;

    public void save(ReqDto reqDto) {
        userMapper.save(reqDto);
    }
}

bookService

@Service
@Log4j2
@DS("common")
public class BookService extends ServiceImpl<BookMapper, Book> {
    @Resource
    private BookMapper bookMapper;

    public void save(ReqDto reqDto) {
        bookMapper.save(reqDto);
    }
}

但是神奇的事发生的,bookService 的数据库应该是 common,但是却是 master 的,也就是说@DS 切换数据源没有起作用 于是开始排查

  • 去除 MasterService.upload 上面的@Transactional,数据源切换正常,但是事务无效
  • BookService 的 save 上面加@Transactional,数据源没有切换
  • BookService 的 save 上面加@Transactional(propagation = Propagation.REQUIRES_NEW),数据源切换,且事务有效

原因:

  • 开启事务的同时,会从数据库连接池获取数据库连接;
  • 如果内层的 service 使用@DS 切换数据源,只是又做了一层拦截,但是并没有改变整个事务的连接;
  • 在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题;
  • 为了使@DS 起作用,必须替换数据库连接,也就是改变事务的传播机智,产生新的事务,获取新的数据库连接;
  • 所以 bookService 的 save 方法上除了加@Transactional 外,还需要设置 propagation = Propagation.REQUIRES_NEW 使得代码走以下逻辑:
private TransactionStatus handleExistingTransaction(
        .....省略.....
  if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
   if (debugEnabled) {
    logger.debug("Suspending current transaction, creating new transaction with name [" +
      definition.getName() + "]");
   }
   SuspendedResourcesHolder suspendedResources = suspend(transaction);
   try {
    return startTransaction(definition, transaction, debugEnabled, suspendedResources);
   }
   catch (RuntimeException | Error beginEx) {
    resumeAfterBeginException(transaction, suspendedResources, beginEx);
    throw beginEx;
   }
  }
  .....省略.....)

在走 startTransaction,再走 doBegin,重新创建新事务,获取新的数据库连接,从而得到@DS 的数据源

startTransaction(definition, transaction, debugEnabled, suspendedResources);

protected void doBegin(Object transaction, TransactionDefinition definition) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
  Connection con = null;

  try {
   if (!txObject.hasConnectionHolder() ||
     txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
    Connection newCon = obtainDataSource().getConnection();//获取数据库连接
    if (logger.isDebugEnabled()) {
     logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
    }
    txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
   }

   txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
   con = txObject.getConnectionHolder().getConnection();

   Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
   txObject.setPreviousIsolationLevel(previousIsolationLevel);
   txObject.setReadOnly(definition.isReadOnly());

   // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
   // so we don't want to do it unnecessarily (for example if we've explicitly
   // configured the connection pool to set it already).
   if (con.getAutoCommit()) {
    txObject.setMustRestoreAutoCommit(true);
    if (logger.isDebugEnabled()) {
     logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
    }
    con.setAutoCommit(false);
   }
   .....省略.....)

最终代码如下,只需要修改的是 bookService bookService

@Service
@Log4j2
@DS("common")
public class BookService extends ServiceImpl<BookMapper, Book> {
    @Resource
    private BookMapper bookMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(ReqDto reqDto) {
        bookMapper.save(reqDto);
    }
}
  1. @DS 数据源切换生效
  2. @Transaction 事务生效

需要注意: master:userService common:bookService

common 数据库的操作,需要在 master 之后,这样当 bookService.save 失败,会使得 userService 回滚; 如果 common 的操作先,那当 userService 失败,无法使 bookService 回滚

会回滚

    @Transactional(rollbackFor = Exception.class)
    public void upload(ReqDto respDto){
        userService.save(respDto);
        bookService.save(respDto);
    }

不会回滚

    @Transactional(rollbackFor = Exception.class)
    public void upload(ReqDto respDto){
        bookService.save(respDto);
        userService.save(respDto);
    }

发布于 2021-09-16 10:49