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

📅 2026/6/30 13:45:22
@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拆到另一个 ServiceServicepublicclassTicketTxService{TransactionalpublicvoidupdateTicketStatus(LongticketId,Stringstatus){ticketMapper.updateStatus(ticketId,status);logMapper.insert(newOperationLog(ticketId,status));}}ServicepublicclassTicketService{AutowiredprivateTicketTxServiceticketTxService;publicvoiddoUpdate(LongticketId,Stringstatus){ticketTxService.updateTicketStatus(ticketId,status);// 走代理事务生效}}// 方案 2注入自己的代理ServicepublicclassTicketService{AutowiredLazyprivateTicketServiceself;publicvoiddoUpdate(LongticketId,Stringstatus){self.updateTicketStatus(ticketId,status);// 走代理}}场景 2方法不是 publicServicepublicclassTicketService{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指定 rollbackForTransactional(rollbackForException.class)// ✅ 所有异常都回滚publicvoidupdateTicketStatus(LongticketId,Stringstatus)throwsException{// ...}// 方案 2抛 RuntimeExceptionTransactionalpublicvoidupdateTicketStatus(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(propagationPropagation.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))ENGINEMyISAM;-- ❌ MyISAM 不支持事务MyISAM 引擎不支持事务InnoDB 才支持。即使代码层面 Transactional 配置正确数据库引擎不支持事务也不生效。解决-- 改成 InnoDBALTERTABLEticketENGINEInnoDB;MySQL 5.5 默认引擎已经是 InnoDB但老项目或手动建表可能还是 MyISAM。检查一下SHOW TABLE STATUS FROM your_db;场景 7Bean 没有被 Spring 管理// 没有 Service 注解Spring 不会管理这个 BeanpublicclassTicketService{Transactional// 没用Spring 根本不知道这个类publicvoidupdateTicketStatus(LongticketId,Stringstatus){// ...}}// 手动 new 出来的也不行TicketServiceservicenewTicketService();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 失效五、CheckListTransactional 上线前排查#检查项风险点正确做法1自调用this 调用绕过代理拆到另一个 Service 或注入代理2方法非 publicAOP 不代理非 public改成 public3异常被 catchSpring 感知不到异常catch 后重新 throw4抛受检异常默认不回滚加 rollbackFor Exception.class5传播行为REQUIRES_NEW 独立事务按业务需要选择传播行为6数据库引擎MyISAM 不支持事务用 InnoDB7Bean 未被管理没有代理对象加 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){ConfigurableApplicationContextctxSpringApplication.run(TransactionalFailApp.class,args);TicketServiceservicectx.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异常被 catchTransactionalpublicvoidtestCatchException(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 修复加 rollbackForTransactional(rollbackForException.class)publicvoidtestCheckedExceptionFixed(LongticketId,Stringstatus)throwsException{ticketMapper.updateStatus(ticketId,status);thrownewException(受检异常加了 rollbackFor 会回滚);}publicStringgetTicketStatus(LongticketId){returnticketMapper.selectStatus(ticketId);}}运行前需要配置数据库MySQL InnoDB 引擎和对应的 Mapper。复现要点自调用testSelfInvoke调this.updateTicketStatus事务失效工单状态被更新异常被 catch事务不回滚工单状态被更新受检异常默认不回滚工单状态被更新加 rollbackFor事务回滚工单状态没变对比场景 3 和场景 4 的结果差异理解异常感知对事务回滚的影响。