ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring 事务失效的常见场景

Spring 事务失效的常见场景 1. 引言Spring 事务管理是 Java 后端开发中最常用的能力之一但也是踩坑重灾区。很多同学在开发中遇到「明明加了Transactional数据却没回滚」的问题排查半天才发现是事务根本没生效。本文梳理 Spring 事务失效的常见场景从自调用、异常类型、多线程、传播行为与隔离级别等角度逐一剖析并给出可运行的实战示例。2. 自调用导致事务失效2.1 问题现象ServicepublicclassOrderService{TransactionalpublicvoidcreateOrder(Orderorder){// 业务逻辑updateStock(order.getProductId());}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidupdateStock(LongproductId){// 扣减库存}}当外部调用createOrder时updateStock上的REQUIRES_NEW并不会生效。原因在于createOrder内部通过this.updateStock()调用走的是this引用而非 Spring 代理对象因此事务注解被绕过。2.2 原理分析Spring 事务基于 AOP 代理实现。只有通过代理对象调用方法时Transactional才会被拦截并织入事务逻辑。自调用this.method()直接调用目标对象自身的方法不经过代理事务注解自然失效。下面是自调用导致事务失效的调用关系图通过代理调用拦截 Transactionalthis.updateStock() 自调用绕过代理事务注解失效正常代理调用REQUIRES_NEW 生效REQUIRES_NEW 不生效外部调用方Spring 代理对象目标方法 createOrder()目标方法 updateStock()目标方法 updateStock()独立事务无独立事务图中实线表示正常经过代理的调用路径虚线表示自调用绕过代理的路径——createOrder内部通过this.updateStock()直接调用目标对象自身的方法没有经过 Spring 代理对象因此REQUIRES_NEW不会生效。2.3 解决方案方案一注入自身代理ServicepublicclassOrderService{AutowiredprivateOrderServiceself;TransactionalpublicvoidcreateOrder(Orderorder){// 业务逻辑self.updateStock(order.getProductId());}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidupdateStock(LongproductId){// 扣减库存}}方案二拆分到不同 BeanServicepublicclassOrderService{AutowiredprivateStockServicestockService;TransactionalpublicvoidcreateOrder(Orderorder){// 业务逻辑stockService.updateStock(order.getProductId());}}方案三使用TransactionTemplate编程式事务ServicepublicclassOrderService{AutowiredprivateTransactionTemplatetransactionTemplate;publicvoidcreateOrder(Orderorder){transactionTemplate.execute(status-{// 业务逻辑updateStock(order.getProductId());returnnull;});}}3. 异常类型不对导致事务不回滚3.1 问题现象TransactionalpublicvoidcreateOrder(Orderorder){try{// 业务逻辑thrownewRuntimeException(库存不足);}catch(Exceptione){// 捕获异常后未抛出log.error(下单失败,e);}}事务方法内部捕获异常后没有重新抛出Spring 认为方法正常返回于是提交事务数据没有回滚。3.2 原理分析Spring 默认只对RuntimeException和Error回滚对受检异常Exception的子类默认不回滚。同时异常必须从Transactional方法中抛出才能触发回滚逻辑。3.3 解决方案方案一让异常抛出方法TransactionalpublicvoidcreateOrder(Orderorder){// 业务逻辑thrownewRuntimeException(库存不足);}方案二指定回滚异常类型Transactional(rollbackForException.class)publicvoidcreateOrder(Orderorder)throwsException{// 业务逻辑thrownewException(库存不足);}方案三手动回滚TransactionalpublicvoidcreateOrder(Orderorder){try{// 业务逻辑thrownewRuntimeException(库存不足);}catch(Exceptione){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();log.error(下单失败,e);}}4. 多线程环境下事务不传播4.1 问题现象TransactionalpublicvoidbatchProcess(ListOrderorders){orders.parallelStream().forEach(order-{// 每个子线程中的数据库操作不在同一事务中updateOrder(order);});}4.2 原理分析Spring 事务默认绑定在当前线程的ThreadLocal中。新开的子线程无法继承父线程的事务上下文因此子线程中的数据库操作各自独立提交不参与父事务。4.3 解决方案方案一避免在事务中开多线程将多线程处理放在事务方法之外或使用TransactionTemplate在子线程中手动开启事务。方案二子线程中使用编程式事务publicvoidbatchProcess(ListOrderorders){orders.parallelStream().forEach(order-{transactionTemplate.execute(status-{updateOrder(order);returnnull;});});}方案三使用TransactionSynchronizationManager手动传递事务上下文不推荐复杂且易出错5. 传播行为与隔离级别实战5.1 传播行为速查传播行为说明REQUIRED默认当前有事务则加入没有则新建REQUIRES_NEW无论当前有无事务都新建一个独立事务SUPPORTS当前有事务则加入没有则以非事务方式执行NOT_SUPPORTED以非事务方式执行挂起当前事务MANDATORY当前必须有事务否则抛异常NEVER当前必须没有事务否则抛异常NESTED嵌套事务基于 Savepoint 实现5.2 隔离级别速查隔离级别说明DEFAULT使用数据库默认隔离级别READ_UNCOMMITTED读未提交存在脏读READ_COMMITTED读已提交解决脏读REPEATABLE_READ可重复读解决不可重复读SERIALIZABLE串行化解决幻读性能最低5.3 实战REQUIRES_NEW 与 REQUIRED 的区别ServicepublicclassOrderService{AutowiredprivateOrderDaoorderDao;AutowiredprivateLogServicelogService;TransactionalpublicvoidcreateOrder(Orderorder){orderDao.insert(order);// 即使这里抛出异常logService 的日志记录已独立提交logService.writeLog(创建订单order.getId());thrownewRuntimeException(业务异常);}}ServicepublicclassLogService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidwriteLog(Stringmessage){// 独立事务不受外层回滚影响}}5.4 实战NESTED 与 REQUIRES_NEW 的选择TransactionalpublicvoidcreateOrder(Orderorder){try{orderDao.insert(order);// NESTED外层回滚时这里也会回滚Savepoint 回滚// REQUIRES_NEW外层回滚时这里不会回滚couponService.deductCoupon(order.getCouponId());}catch(Exceptione){// 业务降级处理}}选择建议需要「部分回滚」且参与外层事务的最终一致性 → 使用NESTED。需要「独立提交」且不受外层事务影响如审计日志 → 使用REQUIRES_NEW。6. 事务失效排查清单遇到事务不生效的问题时可以按下面的清单逐项排查快速定位根因排查项快速验证方法常见误区1. 方法是否为public检查方法修饰符Transactional只对public方法生效误以为private/protected方法也能被代理拦截2. 是否通过代理调用确认调用方注入的是 Bean 而非this自调用会绕过代理在同类内部用this.method()调用事务注解失效3. 异常是否被吞掉检查方法内是否有try-catch且未重新抛出捕获异常后只记日志不抛出Spring 认为方法正常返回4. 异常类型是否为RuntimeException确认抛出的异常是RuntimeException或Error或已配置rollbackFor受检异常默认不回滚需显式指定rollbackFor Exception.class5. 是否在多线程中调用检查是否使用parallelStream、Async或手动new Thread子线程不共享父线程的ThreadLocal事务上下文6. 事务管理器是否配置正确确认DataSourceTransactionManager已注入且数据源正确多个数据源时未指定transactionManager导致事务管理错乱6. 总结Spring 事务失效的常见场景可以归纳为四类自调用this.method()绕过代理事务注解失效。异常类型不对受检异常默认不回滚异常被吞掉也不回滚。多线程事务上下文绑定线程子线程不共享父事务。传播行为与隔离级别使用不当REQUIRES_NEW与NESTED语义不同隔离级别影响并发一致性。掌握这些场景的成因与解决方案能帮助你在日常开发中快速定位事务问题写出更健壮的代码。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进