在实际开发中,我们有时会遇到事务回滚的问题,尤其是在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
工具类进行统一管理。尽管catch
了saveBatch
方法的异常,但实际情况却不同: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
标记,导致最终回滚。
解决方案
为解决该问题,尝试以下几种方案:
- 改用无事务方法:使用
save
方法替代saveBatch
,避免Mybatis-Plus的@Transactional
注解干扰。 - 使用其他批量保存方法:如
Db.saveBatch
方法,可以绕开事务管理器对rollbackOnly
标记的干扰。 - 异步线程处理:在
try
块内的代码使用异步线程处理,从而隔离当前事务。
通过这些方案,我们能够成功避免事务在不必要的情况下被回滚。
总结
在多层嵌套事务中,Mybatis-Plus的@Transactional
注解容易引发意外事务回滚问题。建议在数据层慎用事务注解,尤其是在批量操作时。此外,开发者可以参考Mybatis-Plus官方文档及社区讨论,进一步了解该问题的具体表现及最新解决方案。