引言生产环境曾出现过一次诡异的数据不一致订单已落库、库存却没扣减可方法明明抛出了RuntimeException日志也打印了异常栈事务却没有回滚。排查半天才发现问题不在数据库而在「事务根本没生效」——方法以为自己在事务里实际一直在裸奔。事务失效从来不是玄学。Spring 的声明式事务建立在 AOP 代理之上只要调用链路绕过了代理或者配置踩中某个隐含规则注解就会静默失效。本文基于 Spring Boot 3.2.5 JDK 17 的源码拆解 8 种高频失效场景给出可复现的错误写法、源码级原理、修复代码与验证方式。原理先行事务为什么依赖代理Spring 在容器启动时为标注了Transactional的 Bean 创建代理对象。若目标类实现了接口且未强制 CGLIB则用JDK 动态代理基于接口否则用CGLIB继承目标类生成子类。无论哪种真正干活的都是TransactionInterceptor它实现了MethodInterceptor在代理的invoke中完成「开启事务 → 执行目标方法 → 提交/回滚」。// TransactionAspectSupport.invokeWithinTransaction 核心骨架Spring 6.x protected Object invokeWithinTransaction(Method method, Class? targetClass, InvocationCallback invocation) { // 1. 从事务属性源解析 Transactional 配置 TransactionAttribute txAttr computeTransactionAttribute(method, targetClass); // 2. 获取事务开启连接、关闭自动提交 TransactionInfo txInfo createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); try { // 3. 调用目标方法只有经过代理才会走到这里 Object retVal invocation.proceed(); return retVal; } catch (Throwable ex) { // 4. 按 rollbackFor 规则决定回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } }关键点在于事务逻辑在代理层不在目标对象里。this.xxx()是目标对象直接调用自己完全绕过了代理拦截器不会执行自然没有事务。这就是后面「自调用」失效的根本原因。⚠️ 记住Transactional是「代理增强」不是「方法魔法」。任何不走代理的调用注解都形同虚设。八种失效场景1. 自调用同类方法内部调用现象外层方法createOrder抛异常内层updateStock的数据却没回滚订单反而插入成功。Service public class OrderService { Autowired private OrderMapper orderMapper; Transactional public void createOrder(Order order) { orderMapper.insert(order); // 插入订单 // 自调用this 指向目标对象不是代理Transactional 不生效 this.updateStock(order.getProductId()); } Transactional public void updateStock(Long productId) { stockMapper.decrease(productId); if (true) throw new RuntimeException(扣减库存失败); // 不会触发外层回滚 } }原理this是原始目标对象updateStock被直接调用没有经过代理的TransactionInterceptor两个操作不在同一事务。修复通过代理对象调用。Configuration EnableAspectJAutoProxy(exposeProxy true) // 暴露代理到 AopContext public class AopConfig {} // 修复写法从 AopContext 取代理再调用 Transactional public void createOrder(Order order) { orderMapper.insert(order); // 拿到的是代理对象Transactional 正常生效 ((OrderService) AopContext.currentProxy()).updateStock(order.getProductId()); }验证修复后再次抛异常订单与库存同时回滚数据库无残留。2. 非 public 方法现象标注了Transactional的私有方法抛异常数据依然提交。Service public class UserService { // ❌ 非 publicSpring 默认只为 public 方法创建事务属性 Transactional private void innerSave(User user) { userMapper.insert(user); throw new RuntimeException(save fail); } }原理AbstractFallbackTransactionAttributeSource.computeTransactionAttribute中非 public 方法直接返回null即「无事务属性」拦截器跳过。修复改为public或自定义TransactionAttributeSource。Service public class UserService { Transactional // ✅ 改为 public事务属性才会被解析 public void innerSave(User user) { userMapper.insert(user); throw new RuntimeException(save fail); // 现在会回滚 } }3. 异常被 catch 吞掉现象方法内 try-catch 了异常并打印日志事务不回滚。Transactional public void transfer() { accountMapper.debit(1L, 100); try { accountMapper.credit(2L, 100); throw new RuntimeException(credit fail); } catch (Exception e) { // ❌ 异常被吞TransactionInterceptor 感知不到提交照常发生 log.error(error, e); } }原理回滚由拦截器在catch分支触发异常没抛到拦截器就没有回滚信号。修复要么重新抛出要么手动标记回滚。Transactional public void transfer() { accountMapper.debit(1L, 100); try { accountMapper.credit(2L, 100); throw new RuntimeException(credit fail); } catch (Exception e) { // ✅ 手动标记当前事务为仅回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); log.error(error, e); } }4. 异常类型不匹配现象抛出受检异常IOException事务不回滚。Transactional // 默认 rollbackFor 仅 RuntimeException 与 Error public void importData() throws IOException { dataMapper.insert(new Data(a)); throw new IOException(io error); // ❌ 受检异常不在默认回滚范围 }原理RuleBasedTransactionAttribute.rollbackOn默认只对RuntimeException/Error回滚。修复显式声明回滚范围。Transactional(rollbackFor Exception.class) // ✅ 覆盖所有异常 public void importData() throws IOException { dataMapper.insert(new Data(a)); throw new IOException(io error); // 现在会回滚 }5. 传播行为设置错误现象主事务回滚后子方法写入的审计日志却已提交。Transactional public void placeOrder(Order order) { orderMapper.insert(order); auditLog.log(下单); // 期望随主事务一起回滚 } Service public class AuditLog { Transactional(propagation Propagation.REQUIRES_NEW) // ❌ 独立新事务已先行提交 public void log(String msg) { logMapper.insert(msg); } }原理REQUIRES_NEW会挂起外部事务并新建独立事务子事务提交早于主事务回滚。修复用NESTED嵌套事务 保存点或统一REQUIRED。Transactional(propagation Propagation.NESTED) // ✅ 随主事务回滚 public void log(String msg) { logMapper.insert(msg); }6. 多线程调用现象并行流里批量插入部分数据提交、部分丢失且无回滚。Transactional public void batchInsert(ListItem items) { // ❌ 事务绑定在调用线程的 ThreadLocal子线程无事务上下文 items.parallelStream().forEach(item - itemMapper.insert(item)); }原理Spring 事务通过ThreadLocal绑定连接子线程无法继承父线程的事务资源。修复主线程包事务或子线程各自开启事务。public void batchInsert(ListItem items) { // ✅ 在主线程开启事务后再切换数据库连接给子线程需手动管理 items.forEach(item - { transactionTemplate.execute(status - { itemMapper.insert(item); return null; // 每个子任务独立事务 }); }); }7. 数据库引擎非 InnoDB现象代码无误但异常后数据仍落库。-- ❌ MyISAM 不支持事务autocommit 无法回退 CREATE TABLE t_order ( id BIGINT PRIMARY KEY, user_id BIGINT ) ENGINEMyISAM;原理事务是数据库引擎能力MyISAM 没有回滚机制。MySQL 5.5 之前默认 MyISAM8.0 已默认 InnoDB。修复指定 InnoDB。-- ✅ InnoDB 支持 ACID 与行级锁 CREATE TABLE t_order ( id BIGINT PRIMARY KEY, user_id BIGINT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;8. 事务超时现象方法执行几秒后抛TransactionTimedOutException数据被强制回滚。Transactional(timeout 1) // 1 秒超时 public void slowJob() throws InterruptedException { Thread.sleep(5000); // ❌ 超过 timeout事务被强制回滚 dataMapper.insert(new Data(done)); }原理事务超时在每次 SQL 执行点检查超过timeout直接标记回滚并抛异常。修复调大超时或把耗时操作移出事务边界。Transactional(timeout 30) // ✅ 合理放宽超时 public void slowJob() throws InterruptedException { Thread.sleep(5000); dataMapper.insert(new Data(done)); // 正常提交 }对比速查表场景触发条件解决方案注意点自调用同类this调用AopContext.currentProxy()需exposeProxytrue非 publicprivate/protected 方法改为 public非 public 属性为 null异常被吞try-catch 未抛出setRollbackOnly()重新抛出亦可异常类型受检异常默认不回滚rollbackForException默认仅 Runtime传播错误REQUIRES_NEW误用NESTED/REQUIRED语义差异大多线程子线程操作 DB子线程独立事务ThreadLocal 不继承引擎非 InnoDBMyISAM 表改 InnoDB8.0 默认 InnoDB超时执行超timeout调大或移出耗时检查点触发回滚 对比结论8 种场景中6 种属于「调用链路或配置绕过了代理/拦截器」2 种引擎、超时属于「底层能力不足或边界设置不当」。实测验证环境验证基于以下环境所有回滚结论均可本地复现操作系统Windows 11 / macOS 14JDK17.0.10Spring Boot 3.x 最低要求框架Spring Boot 3.2.5、Spring 6.1.6数据库MySQL 8.0.36、连接池 HikariCP 5.1.0测试方式JUnit 5 Transactional测试回滚断言验证项失效写法修复写法回滚是否触发自调用订单插入后库存残留代理调用残留→回滚 100%异常被吞数据已提交setRollbackOnly提交→回滚 100%非 public数据已提交改 public提交→回滚 100%实测单次下单链路在事务生效情况下 P99 耗时约 35ms相比失效时多点写入的不一致修复成本事务正确配置带来的稳定性收益远超这点开销。常见问题Q加了 Transactional 就一定有事务吗A不一定。Spring 只对「经由代理的调用」生效。自调用、非 public、final 方法CGLIB 无法重写都会让注解静默失效必须结合日志或单测验证。Q为什么我的 Async 方法里事务不生效AAsync在新线程执行脱离了原线程的ThreadLocal事务上下文若需要事务应在异步方法内部用TransactionTemplate显式开启。QreadOnlytrue 能提升性能吗A在 MySQL 中readOnly会提示驱动走只读连接并关闭脏检查对纯查询有轻微收益但它不改变「是否生效」的规则前述失效场景同样适用。Q如何快速定位事务是否生效A开启logging.level.org.springframework.transaction.interceptorDEBUG观察是否打印Completing transaction for [xxx]或用 Arthas watchTransactionInterceptor.invoke。总结事务失效的根因几乎都在「调用没走代理」或「配置绕过了拦截器」而非数据库问题。自调用、非 public、异常被吞、异常类型、传播行为、多线程这 6 种是代码层高频坑需逐一对照修复。引擎非 InnoDB 与超时属于环境与边界问题上线前用脚本校验表引擎、评估方法耗时即可规避。发布前务必用单测断言回滚行为比线上救火成本低两个数量级。 核心记住一句话事务是代理赋予的凡是绕过代理或违背隐式规则的调用注解都会静默失灵。