大家好,我是东哥。
在日常开发中,我们总会遇到一些看似简单,实则暗藏玄机的坑。
今天,我就跟大家分享一个我踩过的坑,关于 MyBatis-Plus 和 @Transactional 注解的使用,看看怎么一个不小心就被“坑”了。
# 问题的起因
某天,我正舒舒服服地喝着咖啡,测试小哥突然冲过来,甩给我一个截图:“兄弟,这个功能炸了!”
截图上赫然显示:
Transaction rolled back because it has been marked as rollback-only
这个错误说白了就是事务被回滚了,因为已经被标记为“只能回滚”。我心想:“哎呀,这不就是经典的嵌套事务问题吗?还好我之前研究过。”
# 经典的嵌套事务问题
在嵌套事务中,如果内层事务出了问题,它不能自行决定回滚或提交,而只能“打个标记”告诉外层事务自己已经完蛋了。等到外层事务收尾时,发现内层事务已经回不来了,只好整体回滚。
我们来看一个简单的示例:
@Servicepublic class UserService {
@Autowired private AddressService addressService;
@Transactional public void createUser() { // 创建用户逻辑 addressService.errorInvoker(); }}
@Servicepublic class AddressService {
@Transactional public void errorInvoker() { // 出错逻辑 throw new RuntimeException("故意出错"); }}
在这个例子中,UserService#createUser 方法标记了 @Transactional,并调用了 AddressService#errorInvoker 方法,该方法也标记了 @Transactional。当 errorInvoker 方法发生异常时,内层事务标记为回滚,而外层事务也因此无法提交,最后只能全部回滚。
# 实际发生的问题
这时,我拍着胸脯对测试小哥说:“这绝对不是我的问题!一定是上次老王留下的锅。”
老王从工位上抬起头,淡定地说:“老子都两个月没碰这个项目了,别胡扯了。”
看到小哥又递来更详细的错误信息,我不得不低头认错,因为问题确实出在我最近写的新代码:
@Override@Transactional(rollbackFor = Exception.class)public Boolean updateRecords(RecordDto dto) { List<Object> list1 = ...; try { // 批量保存list1 } catch (Exception e) { if (e instanceof DuplicateKeyException) { // 过滤重复 key 的数据 // 保存过滤后的list1 } } sendToMQ(dto); List<Object> list2 = ...; try { // 批量保存list2 } catch (Exception e) { if (e instanceof DuplicateKeyException) { // 过滤重复 key 的数据 // 保存过滤后的list2 } } sendToMQ(dto); return Boolean.TRUE;}
这个接口是个“临时工”,用来批量处理一些数据。为了防止重复数据的影响,我专门处理了重复 key。结果测试环境数据来回折腾,导致重复数据满天飞,问题就暴露了出来。
这段代码并不复杂,显然没有嵌套事务的问题,那为什么会出错呢?
# 解决问题的过程
在折腾了一圈之后,我决定冷静下来,静下心来分析问题。
我注意到 try-catch 里面用的是 MyBatis-Plus 的 saveBatch 方法,突然一个灵光闪过——难道是 saveBatch 做了什么“小动作”?果然,一查源码,发现 MyBatis-Plus 默认在 saveBatch 上加了 @Transactional 注解。
public boolean saveBatch(Collection<T> entityList) { return this.saveBatch(entityList, 1000);}
MyBatis-Plus 的这个“善意”设计,原本是为了确保批量操作的事务性,但在我的场景中反倒成了“罪魁祸首”。当 saveBatch 方法遇到异常,事务状态被标记为回滚。即便外层的 try-catch 捕获了异常,事务状态也回不来了。
# 解决方案
最后,问题的解决方案虽然简单,但依然让人感慨:
移除 saveBatch 的 @Transactional 注解:这是 MyBatis-Plus 源码的一部分,无法更改。
修改事务传播机制:调整 saveBatch 的传播机制为 REQUIRES_NEW 或 NESTED,但同样无法在 MyBatis-Plus 源码中直接实现。
自定义批量插入:通过自定义批量插入方法来规避问题。
于是,我写了个简单的批量插入方法,跳过了 MyBatis-Plus 的 saveBatch 限制:
public boolean customSaveBatch(Collection<T> entityList) { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { for (T entity : entityList) { sqlSession.insert("com.example.mapper.insert", entity); } sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); throw e; } finally { sqlSession.close(); } return true;}
这段代码直接使用 SqlSession 的批量操作能力来实现批量插入,避开了 @Transactional 带来的问题。
代码提交后,我对测试小哥说:“你再试试,这不是我的 bug,是框架的问题。”
# 最后的思考
在这个事件中,我再次体会到使用第三方库时保持警惕的重要性。即使是像 MyBatis-Plus 这样成熟的框架,也可能在特定场景下带来意想不到的问题。想到一个段子:有人用网上一个小组件,结果异常时系统直接挂掉,查原因才发现组件里有个不起眼的 System.exit。
编程中,细节决定成败。在处理事务时,要仔细了解事务的传播机制和外部框架的实现细节,这样才能避免被坑。希望这次分享能为大家提供一些有用的经验。
总结一下,在事务管理中要小心使用 @Transactional 注解,特别是嵌套事务时。了解事务的传播机制,适时调整事务策略,是保障系统稳定性的重要手段。
领取专属 10元无门槛券
私享最新 技术干货