疾风步行者

V1

2023/02/16阅读:36主题:自定义主题1

随手的事务注解,竟然让多数据源失效

背景

以前运行好好的代码,最近发现原来的多数据源不起作用了,在仔细回想最近上线的需求后,竟是随手加的一个事务造成的。

调式

由于使用了微服务,会有多个数据库的情况,有时业务需要,需要切换数据源,所以使用了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<UserMapperUser{
    @Resource
    private UserMapper userMapper;
    
    public void save(ReqDto reqDto) {
        userMapper.save(reqDto);
    }
}

bookService

@Service
@Log4j2
@DS("common")
public class BookService extends ServiceImpl<BookMapperBook{
    @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<BookMapperBook{
    @Resource
    private BookMapper bookMapper;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(ReqDto reqDto) {
        bookMapper.save(reqDto);
    }
}

@DS数据源切换生效

@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);
}

整理 | 阿提说说
来源 | http://t.csdn.cn/FRP4i

作者《Prometheus+Grafana 实践派》专栏火热更新中


关注我,给你看更多精彩文章。

分类:

后端

标签:

后端

作者介绍

疾风步行者
V1

还在奔跑的老程序员