news 2026/7/7 17:25:10

@Transactional 失效的 7 种场景:第 5 种最难排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@Transactional 失效的 7 种场景:第 5 种最难排查

@Transactional 失效的 7 种场景:第 5 种最难排查

加了 @Transactional 事务还是没回滚?不是注解的问题,是你的用法有问题。7 种失效场景,你至少踩过 3 个。


一、事故现场

线上有个工单状态更新接口,先更新工单状态,再写操作日志。两个操作要在同一个事务里,要么都成功,要么都回滚。

@ServicepublicclassTicketService{@AutowiredprivateTicketMapperticketMapper;@AutowiredprivateOperationLogMapperlogMapper;@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);// 更新工单状态logMapper.insert(newOperationLog(ticketId,status));// 写操作日志}}

测试环境跑得好好的。上线后某天,工单状态更新了,但操作日志没写进去。事务没回滚,@Transactional 失效了。

排查发现:这个方法被同一个类的另一个方法调用了(自调用),@Transactional 的 AOP 代理没生效。


二、@Transactional 为什么会失效

@Transactional 的原理是 Spring AOP 动态代理。Spring 在 Bean 初始化时生成一个代理对象,代理对象在方法执行前后管理事务的开启和提交。

凡是绕过代理对象的调用,@Transactional 都不生效。

这个原理决定了 7 种失效场景,下面逐个讲。


三、7 种失效场景

场景 1:自调用(最常见)

@ServicepublicclassTicketService{@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);logMapper.insert(newOperationLog(ticketId,status));}publicvoiddoUpdate(LongticketId,Stringstatus){// 自调用:this 调用,走的是原始对象不是代理对象updateTicketStatus(ticketId,status);// @Transactional 失效!}}

doUpdate调用this.updateTicketStatus()this是原始对象不是 Spring 代理对象。代理拦截不到,事务不生效。

解决:

// 方案 1:拆到另一个 Service@ServicepublicclassTicketTxService{@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);logMapper.insert(newOperationLog(ticketId,status));}}@ServicepublicclassTicketService{@AutowiredprivateTicketTxServiceticketTxService;publicvoiddoUpdate(LongticketId,Stringstatus){ticketTxService.updateTicketStatus(ticketId,status);// 走代理,事务生效}}// 方案 2:注入自己的代理@ServicepublicclassTicketService{@Autowired@LazyprivateTicketServiceself;publicvoiddoUpdate(LongticketId,Stringstatus){self.updateTicketStatus(ticketId,status);// 走代理}}

场景 2:方法不是 public

@ServicepublicclassTicketService{@TransactionalvoidupdateTicketStatus(LongticketId,Stringstatus){// 包级私有!ticketMapper.updateStatus(ticketId,status);logMapper.insert(newOperationLog(ticketId,status));}}

Spring AOP 默认只代理 public 方法。protected、private、包级私有方法上的 @Transactional 不生效。

解决:改成 public。

@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){// ✅ public// ...}

场景 3:异常被 catch 了

@ServicepublicclassTicketService{@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);try{logMapper.insert(newOperationLog(ticketId,status));}catch(Exceptione){log.error("写日志失败",e);// 异常被吞了!// 事务不会回滚,因为 Spring 没看到异常}}}

Spring 通过捕获方法抛出的异常来判断是否回滚。异常被 catch 了,Spring 看不到异常,认为方法正常执行完,提交事务。

解决:

@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);try{logMapper.insert(newOperationLog(ticketId,status));}catch(Exceptione){log.error("写日志失败",e);throwe;// ✅ 重新抛出,让 Spring 感知到异常}}

场景 4:异常类型不对(最难排查)

@ServicepublicclassTicketService{@Transactional// 默认只回滚 RuntimeException 和 ErrorpublicvoidupdateTicketStatus(LongticketId,Stringstatus)throwsException{ticketMapper.updateStatus(ticketId,status);if(!status.equals("VALID")){thrownewException("状态不合法");// 受检异常,不回滚!}logMapper.insert(newOperationLog(ticketId,status));}}

@Transactional 默认只回滚 RuntimeException 和 Error,不回滚受检异常(checked exception)。throw new Exception()是受检异常,Spring 默认不会回滚。

这是最坑的场景。代码看起来没问题,异常也抛了,但事务就是不回滚。而且不会报错,只在数据层面出问题,排查极难。

解决:

// 方案 1:指定 rollbackFor@Transactional(rollbackFor=Exception.class)// ✅ 所有异常都回滚publicvoidupdateTicketStatus(LongticketId,Stringstatus)throwsException{// ...}// 方案 2:抛 RuntimeException@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);if(!status.equals("VALID")){thrownewRuntimeException("状态不合法");// ✅ RuntimeException 会回滚}// ...}

建议养成习惯:所有 @Transactional 都加rollbackFor = Exception.class。宁可多回滚,不可漏回滚。

场景 5:事务传播行为不对

@ServicepublicclassTicketService{@AutowiredprivateLogServicelogService;@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);logService.writeLog(ticketId,status);// REQUIRES_NEW,独立事务提交thrownewRuntimeException("更新失败");// 外层事务回滚}}@ServicepublicclassLogService{@Transactional(propagation=Propagation.REQUIRES_NEW)// 新开事务publicvoidwriteLog(LongticketId,Stringstatus){logMapper.insert(newOperationLog(ticketId,status));}}

LogService.writeLog用了REQUIRES_NEW,会挂起当前事务,新开一个独立事务。writeLog 执行完,内层事务就提交了。随后外层抛异常回滚,但内层事务已经提交了,不会跟着回滚。工单状态更新被回滚,操作日志却留下来了。

如果这里的日志不是"必须留痕"而是"必须跟主业务一致",就会出现数据不一致。这就是 REQUIRES_NEW 的陷阱:内外层事务独立,一个回滚不影响另一个。

反过来也一样:如果内层事务抛异常回滚,异常会传播到外层,外层如果不 catch 也会回滚。别以为用了 REQUIRES_NEW 就互不影响了。

解决:理解传播行为,按业务需要选择。

传播行为行为什么时候用
REQUIRED(默认)有事务就加入,没有就新建99% 的场景
REQUIRES_NEW挂起当前事务,新建独立事务日志记录(即使主事务回滚日志也要留)
NESTED嵌套事务(基于 savepoint)部分回滚场景
SUPPORTS有事务就加入,没有就非事务执行查询方法
NOT_SUPPORTED非事务执行,挂起当前事务不需要事务的操作
MANDATORY必须在事务中,否则报错强制要求调用方有事务
NEVER非事务执行,有事务则报错不允许在事务中执行

场景 6:数据库引擎不支持事务

-- 建表时用了 MyISAM 引擎CREATETABLEticket(idBIGINTPRIMARYKEY,statusVARCHAR(20))ENGINE=MyISAM;-- ❌ MyISAM 不支持事务

MyISAM 引擎不支持事务,InnoDB 才支持。即使代码层面 @Transactional 配置正确,数据库引擎不支持,事务也不生效。

解决:

-- 改成 InnoDBALTERTABLEticketENGINE=InnoDB;

MySQL 5.5+ 默认引擎已经是 InnoDB,但老项目或手动建表可能还是 MyISAM。检查一下:SHOW TABLE STATUS FROM your_db;

场景 7:Bean 没有被 Spring 管理

// 没有 @Service 注解,Spring 不会管理这个 BeanpublicclassTicketService{@Transactional// 没用,Spring 根本不知道这个类publicvoidupdateTicketStatus(LongticketId,Stringstatus){// ...}}// 手动 new 出来的也不行TicketServiceservice=newTicketService();service.updateTicketStatus(1L,"VALID");// @Transactional 失效

@Transactional 依赖 Spring 容器创建代理对象。如果类没有被 Spring 管理(没加 @Service/@Component),或者手动 new 的,Spring 没机会生成代理。

解决:加上 @Service 或 @Component,通过依赖注入使用。


四、一张图总结

@Transactional 生效的前提: │ ├─ 1. 方法是 public ──→ 非 public 不代理 │ ├─ 2. 通过代理对象调用 ──→ 自调用 / 手动 new 失效 │ ├─ 3. 异常抛到代理层 ──→ catch 吞异常失效 │ ├─ 4. 异常类型匹配 ──→ 受检异常默认不回滚(加 rollbackFor) │ ├─ 5. 传播行为正确 ──→ REQUIRES_NEW 会独立提交/回滚 │ ├─ 6. 数据库支持事务 ──→ MyISAM 不支持 │ └─ 7. Bean 被 Spring 管理 ──→ 没注解 / 手动 new 失效

五、CheckList:@Transactional 上线前排查

#检查项风险点正确做法
1自调用this 调用绕过代理拆到另一个 Service 或注入代理
2方法非 publicAOP 不代理非 public改成 public
3异常被 catchSpring 感知不到异常catch 后重新 throw
4抛受检异常默认不回滚加 rollbackFor = Exception.class
5传播行为REQUIRES_NEW 独立事务按业务需要选择传播行为
6数据库引擎MyISAM 不支持事务用 InnoDB
7Bean 未被管理没有代理对象加 @Service,通过注入使用

六、总结

7 种失效场景,记住核心原理:

@Transactional 依赖 Spring AOP 代理。凡是不经过代理的调用、代理拦截不到的异常、不匹配的回滚条件,都会让事务失效。

养成三个习惯:

  • 所有 @Transactional 加rollbackFor = Exception.class
  • 事务方法不要自调用,拆到不同的 Service
  • 异常别吞,catch 了就 throw 出去

附录:本地复现完整代码

importorg.springframework.stereotype.Service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.transaction.annotation.Propagation;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublicclassTransactionalFailApp{publicstaticvoidmain(String[]args){ConfigurableApplicationContextctx=SpringApplication.run(TransactionalFailApp.class,args);TicketServiceservice=ctx.getBean(TicketService.class);// 测试 1:自调用失效System.out.println("\n=== 场景1:自调用 ===");try{service.testSelfInvoke(1L,"CLOSED");}catch(Exceptione){System.out.println("异常: "+e.getMessage());}System.out.println("工单状态: "+service.getTicketStatus(1L));// 如果事务生效,状态不应该被更新// 如果事务失效,状态被更新了(因为自调用绕过了代理)// 测试 2:异常被 catchSystem.out.println("\n=== 场景3:异常被catch ===");try{service.testCatchException(1L,"CLOSED");}catch(Exceptione){System.out.println("异常: "+e.getMessage());}System.out.println("工单状态: "+service.getTicketStatus(1L));// 异常被吞,事务不回滚,状态被更新// 测试 3:受检异常不回滚System.out.println("\n=== 场景4:受检异常 ===");try{service.testCheckedException(1L,"CLOSED");}catch(Exceptione){System.out.println("异常: "+e.getMessage());}System.out.println("工单状态: "+service.getTicketStatus(1L));// 受检异常默认不回滚,状态被更新// 测试 4:加 rollbackFor 后回滚System.out.println("\n=== 场景4修复:rollbackFor ===");try{service.testCheckedExceptionFixed(1L,"CLOSED");}catch(Exceptione){System.out.println("异常: "+e.getMessage());}System.out.println("工单状态: "+service.getTicketStatus(1L));// 加了 rollbackFor,事务回滚,状态没变ctx.close();}}@ServiceclassTicketService{@AutowiredprivateTicketMapperticketMapper;@AutowiredprivateLogMapperlogMapper;// 场景 1:自调用@TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);logMapper.insert(newOperationLog(ticketId,status));}publicvoidtestSelfInvoke(LongticketId,Stringstatus){updateTicketStatus(ticketId,status);// 自调用,事务失效}// 场景 3:异常被 catch@TransactionalpublicvoidtestCatchException(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);try{thrownewRuntimeException("故意抛异常");}catch(Exceptione){System.out.println("异常被吞了: "+e.getMessage());// 没有 throw,事务不回滚}}// 场景 4:受检异常,默认不回滚@TransactionalpublicvoidtestCheckedException(LongticketId,Stringstatus)throwsException{ticketMapper.updateStatus(ticketId,status);thrownewException("受检异常,默认不回滚");}// 场景 4 修复:加 rollbackFor@Transactional(rollbackFor=Exception.class)publicvoidtestCheckedExceptionFixed(LongticketId,Stringstatus)throwsException{ticketMapper.updateStatus(ticketId,status);thrownewException("受检异常,加了 rollbackFor 会回滚");}publicStringgetTicketStatus(LongticketId){returnticketMapper.selectStatus(ticketId);}}

运行前需要配置数据库(MySQL + InnoDB 引擎)和对应的 Mapper。

复现要点:

  1. 自调用:testSelfInvokethis.updateTicketStatus,事务失效,工单状态被更新
  2. 异常被 catch:事务不回滚,工单状态被更新
  3. 受检异常:默认不回滚,工单状态被更新
  4. 加 rollbackFor:事务回滚,工单状态没变

对比场景 3 和场景 4 的结果差异,理解"异常感知"对事务回滚的影响。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 9:55:49

d2s-editor:5分钟学会暗黑破坏神2存档编辑,告别复杂十六进制操作

d2s-editor:5分钟学会暗黑破坏神2存档编辑,告别复杂十六进制操作 【免费下载链接】d2s-editor 项目地址: https://gitcode.com/gh_mirrors/d2/d2s-editor 你是否曾经因为暗黑破坏神2存档修改太复杂而放弃调整角色属性?是否因为装备管…

作者头像 李华
网站建设 2026/7/3 7:52:44

选型时怎么分辨产品是真自研还是贴牌整合?企业级AI智能体避坑指南

站在2026年这个数字化转型的关键节点,企业对于AI技术的应用已从最初的“尝鲜”转向了“深度融合”。根据IDC发布的《2026年全球人工智能支出指南》显示,全球在AI系统上的支出预计将超过4500亿美元,而其中企业级AI智能体的部署量较2024年增长了…

作者头像 李华
网站建设 2026/7/7 17:24:59

MTEX:当材料科学家遇见数学魔术师

MTEX:当材料科学家遇见数学魔术师 【免费下载链接】mtex MTEX is a free Matlab toolbox for quantitative texture analysis. Homepage: 项目地址: https://gitcode.com/gh_mirrors/mt/mtex 如果你曾经试图从电子显微镜的像素海洋中解读晶体的秘密语言&…

作者头像 李华
网站建设 2026/7/3 16:39:44

Android逆向实战:巧用核心破解与虚拟环境,无痛绕过APK签名校验

1. 签名校验的江湖恩怨 每次打开那些被重新打包的微信支付宝,看到"参数不合法"的提示,我就想起武侠小说里的"此路不通"牌子。签名校验就像APP给自己装的门禁系统,专门防我们这些"不速之客"。但道高一尺魔高一…

作者头像 李华
网站建设 2026/7/3 16:39:23

如何永久保存微信聊天记录?WeChatMsg完整指南让数据不再丢失

如何永久保存微信聊天记录?WeChatMsg完整指南让数据不再丢失 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we…

作者头像 李华
网站建设 2026/7/3 4:58:32

管理报表与经营分析体系搭建:工具选型指南

管理报表和经营分析体系搭建,核心不在于选一款功能最全的工具,而在于让工具匹配企业的数据基础、业务场景和IT实施能力。先胜业财在服务鲁西化工、华润万家、中信集团等大型企业的过程中发现,选型失败的根本原因往往不是工具本身,…

作者头像 李华