在实际开发中,我们有时会遇到事务回滚的问题,尤其是在Spring的多层嵌套事务中。最近遇到了一次特殊情况,即使用Mybatis-Plus的saveBatch方法时,出现事务被意外回滚的问题。本文将对该问题进行分析并分享解决方法。

问题背景

一个Kafka Consumer消费了上游的消息并写入数据,整个过程被最外层方法用@Transactional(rollbackFor = Exception.class)注解管理,以确保数据一致性。然而,某子业务没有事务需求,前人采用了try catch来防止该业务流影响全局事务。代码简化如下:

@Service
public class ServiceA {
    @Transactional(rollbackFor = Exception.class)
    public void test(String username) {
        // 保存数据
        companyService.save(new Company("家里蹲", "localhost"));
        // 调用方法B
        serviceB.doBusiness(username);
    }
}

@Service
public class ServiceB {
    @Transactional(rollbackFor = Exception.class)
    public void doBusiness(String username) {
        try {
            userService.saveBatch(Collections.singletonList(new User(username, "shenzhen")));
        } catch (DuplicateKeyException e) {
            e.printStackTrace();
        }
    }
}

按理说,saveBatch内部的异常已经被捕获,不应影响外部事务。但我们在日志中发现,不仅代码块中的异常频繁触发,且数据未能成功写入。通过MySQL事务追踪发现,这些数据已被回滚。

问题分析
问题分析

问题分析

Spring通过@Transactional注解来管理事务,通过PlatformTransactionManager工具类进行统一管理。尽管catchsaveBatch方法的异常,但实际情况却不同:Spring事务管理器采用标记位rollbackOnly机制,一旦感知到异常,即使异常被捕获,当前线程的事务状态仍会被标记为rollbackOnly,阻止后续提交。

具体来说,当doBusiness方法的事务在提交时检测到rollbackOnly状态时,会抛出UnexpectedRollbackException异常。这一异常使得调用链上的ServiceA.test方法也进入回滚流程,从而导致整个Kafka Consumer事务的回滚。

源码解析

在源码中,通过搜索异常信息“Transaction rolled back because it has been marked as rollback-only”,定位到AbstractPlatformTransactionManager#processRollback方法。进一步跟踪可以发现,在AbstractPlatformTransactionManager#commit方法中,事务尝试提交时检测到rollbackOnly标记,导致最终回滚。

解决方案

为解决该问题,尝试以下几种方案:

  1. 改用无事务方法:使用save方法替代saveBatch,避免Mybatis-Plus的@Transactional注解干扰。
  2. 使用其他批量保存方法:如Db.saveBatch方法,可以绕开事务管理器对rollbackOnly标记的干扰。
  3. 异步线程处理:在try块内的代码使用异步线程处理,从而隔离当前事务。

通过这些方案,我们能够成功避免事务在不必要的情况下被回滚。

总结

在多层嵌套事务中,Mybatis-Plus的@Transactional注解容易引发意外事务回滚问题。建议在数据层慎用事务注解,尤其是在批量操作时。此外,开发者可以参考Mybatis-Plus官方文档及社区讨论,进一步了解该问题的具体表现及最新解决方案。