首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

警惕!MyBatis-Plus中慎用@Transactional注解,差点被开了...

大家好,我是东哥。

在日常开发中,我们总会遇到一些看似简单,实则暗藏玄机的坑。

今天,我就跟大家分享一个我踩过的坑,关于 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 注解,特别是嵌套事务时。了解事务的传播机制,适时调整事务策略,是保障系统稳定性的重要手段。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OuCarr9H3Fj0dQMfPteHvU2A0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券