文章目录一、引言二、前置知识Spring 事务原理速览三、八大失效场景逐一拆解3.1 场景一数据库引擎不支持事务3.2 场景二类内部方法调用最常见⭐⭐⭐⭐⭐3.3 场景三非 public 方法3.4 场景四异常类型不匹配3.5 场景五异常被 catch 吞掉常见⭐⭐⭐⭐3.6 场景六多线程环境3.7 场景七事务传播行为配置错误3.8 场景八非 Spring 管理的 Bean 中使用四、快速排查指南五、最佳实践总结推荐的事务注解模板代码组织建议单元测试中验证事务六、总结参考一、引言在 Spring Boot 项目中使用事务非常简单——加个Transactional就搞定。但简单往往意味着容易出错。在实际项目中事务不回滚的 bug 屡见不鲜究其原因大多数是对 Spring 事务的底层实现机制理解不够。Spring 声明式事务的核心依赖是AOP 动态代理。你用Transactional标记的方法Spring 会为它生成一个代理对象在方法执行前后自动开启和提交/回滚事务。理解了这个前提就能推导出一个关键结论只有通过 Spring 代理对象调用的方法事务注解才会生效。本文将从这一核心原理出发梳理 8 大常见失效场景每个场景附带可运行的代码示例和正确的修复方式。二、前置知识Spring 事务原理速览在深入失效场景之前先用一张图理解 Spring 事务的工作链路核心流程如下Spring 启动时扫描Transactional注解为对应 Bean 生成代理对象JDK 动态代理或 CGLIB 代理调用方拿到的是代理对象而不是原始对象代理对象调用TransactionInterceptor它从PlatformTransactionManager获取数据库连接开启事务执行目标方法根据方法执行结果是否抛出异常决定 commit 还是 rollback事务状态Connection绑定在ThreadLocal上这解释了为什么多线程场景下事务会失效。了解这个流程后下面逐一剖析失效场景。三、八大失效场景逐一拆解3.1 场景一数据库引擎不支持事务这是最容易被忽略的低级错误。MySQL 的MyISAM引擎不支持事务如果你的表是 MyISAM加再多Transactional也无济于事。// 错误示例表使用 MyISAM 引擎// CREATE TABLE user (id BIGINT, name VARCHAR(50)) ENGINEMyISAM;ServicepublicclassUserService{TransactionalpublicvoidcreateUser(){userMapper.insert(newUser(1L,张三));// 执行成功inti1/0;// 抛出异常// 预期回滚但 MyISAM 不支持事务第一条数据已持久化}}排查方式执行SHOW TABLE STATUS FROM 数据库名查看Engine列确认是 InnoDB。修复方式将表引擎改为 InnoDB。ALTERTABLEuserENGINEInnoDB;3.2 场景二类内部方法调用最常见⭐⭐⭐⭐⭐这是实际项目中出现频率最高的事务失效场景。直接看代码ServicepublicclassOrderService{// ❌ 错误写法外部调用 createOrder()事务不生效publicvoidcreateOrder(Orderorder){// 非事务方法直接调用同类中的事务方法this.saveOrder(order);// ← this 调用绕过了代理sendNotify(order);}TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 假设这里抛异常数据不会回滚}privatevoidsendNotify(Orderorder){// 发送通知...}}原理分析外部调用orderService.createOrder()时拿到的是 CGLIB 代理对象。createOrder()没有事务注解代理对象直接调用目标对象的方法。而目标对象内部的this.saveOrder()是 Java 原生的方法调用它根本不知道代理对象的存在Transactional自然不生效。自调用机制当类中的方法调用同一个类中的另一个Transactional 方法时事务可能不会生效。这是因为事务注解是通过 AOP 实现的而 Spring 的 AOP 代理机制在这种情况下不会被触发。三种正确的修复方式// ✅ 方式一调用方也标记 Transactional推荐最简单ServicepublicclassOrderService{Transactional// 让外层方法也加入事务管理publicvoidcreateOrder(Orderorder){saveOrder(order);// 此时事务已在外层开启内层复用同一事务sendNotify(order);}publicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 异常会回滚因为事务在外层已开启}privatevoidsendNotify(Orderorder){/* ... */}}// ✅ 方式二将事务方法提取到独立的 Service推荐职责清晰ServicepublicclassOrderService{AutowiredprivateOrderPersistenceServicepersistenceService;// 注入另一个 BeanpublicvoidcreateOrder(Orderorder){persistenceService.saveOrder(order);// ← 跨 Bean 调用走代理事务生效sendNotify(order);}}ServicepublicclassOrderPersistenceService{TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 异常会回滚 ✓}}// ✅ 方式三通过 AopContext.currentProxy() 获取代理对象ServicepublicclassOrderService{publicvoidcreateOrder(Orderorder){// 拿到当前类的代理对象再调用事务方法((OrderService)AopContext.currentProxy()).saveOrder(order);sendNotify(order);}TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);}}// 需要在启动类或配置类中开启 exposeProxy// EnableAspectJAutoProxy(exposeProxy true)建议优先使用方法二提取独立 Service职责单一、可测试性强。方法一简单但会让外层方法变重适合逻辑不复杂的场景。3.3 场景三非 public 方法Spring 事务注解默认只对 public 方法生效。如果把Transactional放在 private 或 protected 方法上不会有任何事务。ServicepublicclassUserService{// ❌ 错误private 方法上的事务不生效TransactionalprivatevoidsavePrivate(Useruser){userMapper.insert(user);}// ❌ 错误protected 方法上的事务默认也不生效TransactionalprotectedvoidsaveProtected(Useruser){userMapper.insert(user);}// ✅ 正确只能放在 public 方法上Transactionalpublicvoidsave(Useruser){userMapper.insert(user);}}源码依据Spring 的AbstractFallbackTransactionAttributeSource.computeTransactionAttribute()方法中有这样一段逻辑// org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSourceprotectedTransactionAttributecomputeTransactionAttribute(Methodmethod,Class?targetClass){// 如果方法不是 public 的且 allowPublicMethodsOnly() 返回 true默认为 trueif(allowPublicMethodsOnly()!Modifier.isPublic(method.getModifiers())){returnnull;// ← 直接返回 null不应用事务}// ...}Spring 团队的设计理由是非 public 方法不应该暴露为事务边界。如果你确实有特殊需求可以自定义TransactionAttributeSource但强烈不推荐。3.4 场景四异常类型不匹配Transactional默认只对RuntimeException和Error进行回滚。当你抛出一个 checked exception如IOException、自定义受检异常时事务不会回滚。ServicepublicclassUserService{// ❌ 错误抛出 checked exception事务不会回滚TransactionalpublicvoidbatchImport(InputStreaminput)throwsIOException{userMapper.insert(newUser(1L,张三));// 模拟读取异常if(inputnull){thrownewIOException(文件读取失败);// ← checked exception默认不回滚}}// ✅ 正确显式指定 rollbackForTransactional(rollbackForException.class)// 所有异常都回滚publicvoidbatchImportCorrect(InputStreaminput)throwsIOException{userMapper.insert(newUser(2L,李四));if(inputnull){thrownewIOException(文件读取失败);// 这次会回滚 ✓}}// ✅ 更精确的写法只回滚你关心的特定异常Transactional(rollbackFor{IOException.class,BusinessException.class})publicvoidbatchImportPrecise(InputStreaminput)throwsIOException{// ...}}最佳实践项目中统一使用Transactional(rollbackFor Exception.class)或者在全局配置中设置默认回滚策略避免遗漏。3.5 场景五异常被 catch 吞掉常见⭐⭐⭐⭐这是仅次于内部方法调用的高频失效场景。你把异常 catch 住了但没重新抛出事务管理器根本不知道发生了异常自然就不会回滚。ServicepublicclassOrderService{// ❌ 错误catch 后吞掉异常事务不回滚TransactionalpublicvoidcreateOrder(Orderorder){try{orderMapper.insert(order);inti1/0;// 抛出 ArithmeticException// 其他业务逻辑...}catch(Exceptione){log.error(创建订单失败,e);// ← 异常被吞了方法正常返回Spring 提交事务}}}为什么会这样Spring 的事务拦截器在目标方法执行后检查是否有未捕获的异常从方法中抛出。try-catch 兜底后异常不再往外抛拦截器认为方法正常结束于是 commit。三种正确的修复方式Slf4jServicepublicclassOrderService{// ✅ 方式一catch 之后重新抛出运行时异常TransactionalpublicvoidcreateOrderV1(Orderorder){try{orderMapper.insert(order);inti1/0;}catch(Exceptione){log.error(创建订单失败,e);thrownewRuntimeException(创建订单失败,e);// 重新抛出}}// ✅ 方式二手动标记回滚适合需要返回错误码而非抛异常的场景TransactionalpublicvoidcreateOrderV2(Orderorder){try{orderMapper.insert(order);inti1/0;}catch(Exceptione){log.error(创建订单失败,e);// 手动标记当前事务需要回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 此时可以返回业务错误码不需要抛异常}}// ✅ 方式三精确 catch只处理能处理的异常推荐TransactionalpublicvoidcreateOrderV3(Orderorder){try{orderMapper.insert(order);// 调用外部服务允许失败remoteNotifyService.send(order);}catch(RemoteNotifyExceptione){// 通知服务失败不应该回滚订单事务log.warn(通知发送失败但订单正常创建,e);// 不重新抛出 通知失败不影响订单持久化}// RuntimeException 不要 catch让它自然抛出触发回滚}}核心原则只有你明确知道某个异常不应该导致事务回滚时才 catch 它。如果你不确定不要 catch让它自然抛到代理层。3.6 场景六多线程环境Transactional的事务状态通过ThreadLocal绑定到当前线程。当你开启新线程执行数据库操作时新线程里没有事务上下文自然也享受不到事务保护。ServicepublicclassOrderService{AutowiredprivateOrderMapperorderMapper;// ❌ 错误新线程中的方法没有事务TransactionalpublicvoidcreateOrderAsync(Orderorder){orderMapper.insert(order);// 这条在当前线程的事务中// 在子线程中执行数据库操作——事务不传递newThread(()-{orderMapper.insertDetail(order.getDetail());// ← 子线程事务不生效}).start();// 模拟主线程异常inti1/0;// 结果order 会回滚但 orderDetail 已经写入了在子线程的独立连接中}}为什么会这样主线程的事务是把数据库连接Connection存在ThreadLocal中。子线程是一个全新的 Thread它的ThreadLocal是空的所以orderMapper.insertDetail()会从连接池拿一个新的 Connection以非事务模式auto-commit执行。即使主线程回滚了子线程写入的数据早已持久化。更隐蔽的变体使用AsyncSlf4jServicepublicclassOrderService{AutowiredprivateOrderDetailServicedetailService;TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);// Async 方法在独立线程池中执行事务不传递detailService.saveDetailAsync(order.getDetail());inti1/0;// 同样order 回滚detail 不回滚}}ServiceclassOrderDetailService{Async// ← 异步 新线程事务不传递Transactional// ← 这个事务在新的独立线程中与主线程事务无关publicvoidsaveDetailAsync(OrderDetaildetail){detailMapper.insert(detail);}}正确做法// ✅ 方式一如果需要保证原子性不要用多线程TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);orderMapper.insertDetail(order.getDetail());// 同步执行共享同一事务// 两条数据同时成功或同时回滚 ✓}// ✅ 方式二使用分布式事务方案Seata、RocketMQ 事务消息等// ✅ 方式三改成最终一致性方案——主事务提交后通过消息队列异步处理TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);orderMapper.insertDetail(order.getDetail());// 事务提交成功后再发消息// TransactionSynchronizationManager.registerSynchronization(...)}3.7 场景七事务传播行为配置错误Spring 提供了 7 种传播行为Propagation配置不当会让事务看起来失效。ServicepublicclassUserService{AutowiredprivateLogServicelogService;// ❌ 错误外部事务存在时logService 挂起事务再执行插入的日志不回滚TransactionalpublicvoiddeleteUser(LonguserId){userMapper.delete(userId);logService.recordDeleteLog(userId);// 调用另一个事务方法inti1/0;// 异常user 删除会回滚但日志已写入}}ServiceclassLogService{Transactional(propagationPropagation.REQUIRES_NEW)// ← 挂起当前事务新建独立事务publicvoidrecordDeleteLog(LonguserId){logMapper.insert(newOperationLog(userId,DELETE));// 这个事务已提交外部回滚不影响它}}这不是失效而是设计如此。Propagation.REQUIRES_NEW的语义就是新建一个独立的物理事务。你需要根据业务需求选择合适的传播行为传播行为含义适用场景REQUIRED默认有事务则加入没有则新建绝大多数场景REQUIRES_NEW总是新建事务挂起当前事务日志记录、审计等不应因业务回滚而撤销的操作NESTED嵌套事务内部回滚不影响外部部分回滚时需要保存点savepointSUPPORTS有事务就加入没有就以非事务运行查询为主的只读操作NOT_SUPPORTED以非事务方式运行挂起当前事务不需要事务的操作MANDATORY必须在事务中运行否则抛异常强制调用方提供事务NEVER必须在非事务中运行否则抛异常强制无事务执行排查技巧开启 Spring 事务日志观察事务的创建和提交/回滚行为# application.yml 或 application.propertieslogging:level:org.springframework.transaction.interceptor:DEBUGorg.springframework.orm.jpa:DEBUG3.8 场景八非 Spring 管理的 Bean 中使用Transactional只在 Spring 容器管理的 Bean 中生效。以下几种情况注解会静默失效// ❌ 情况1直接 new 出来的对象publicclassSomeController{AutowiredprivateApplicationContextcontext;publicvoidhandle(){// 直接 new不在 Spring 容器中Transactional 无效OrderServiceservicenewOrderService();service.createOrder(order);// 事务不生效}}// ❌ 情况2在 Configuration 类的方法上加 TransactionalConfigurationpublicclassAppConfig{Transactional// ← 无效配置类的 Bean 初始化方法不是业务调用入口publicDataSourcedataSource(){returnnewHikariDataSource();}}// ❌ 情况3工具类的 static 方法publicclassOrderUtils{Transactional// ← 无效static 方法不能被 AOP 代理拦截publicstaticvoidsaveOrder(Orderorder){// ...}}根本原因Spring AOP 代理只能拦截 Spring 容器中 Bean 的实例方法调用。new出来的对象、static 方法、配置类的Bean方法都不满足这个前提。四、快速排查指南遇到事务不生效时按以下清单逐项自查检查项排查命令 / 方法① 数据库引擎SHOW TABLE STATUS确认 InnoDB② 调用链路是否跨 Bean 调用是否用了this.③ 方法修饰符方法是否为public④ 异常类型是否 catch 吞了抛的是否 RuntimeException⑤ 线程环境是否涉及new Thread()或Async⑥ 传播行为Transactional的propagation属性是否正确⑦ Bean 管理类是否由 Spring 容器管理是否是new出来的⑧ 事务日志开启DEBUG日志观察事务行为最直接的诊断手段——开启日志logging:level:org.springframework.transaction.interceptor:DEBUG你会看到类似这样的输出Creating new transaction with name [com.example.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT Opened new Connection [conn1] for JDBC transaction Initiating transaction commit如果加了Transactional但日志里完全没有Creating new transaction说明注解没生效按清单排查。五、最佳实践总结推荐的事务注解模板ServiceSlf4jpublicclassOrderService{/** * 标准事务方法模板 * - rollbackFor Exception.class确保任何异常都回滚 * - timeout防止长事务锁表 * - readOnly true只读操作标记提升性能 */Transactional(rollbackForException.class,timeout30,readOnlytrue)publicOrdergetOrder(LongorderId){returnorderMapper.selectById(orderId);}Transactional(rollbackForException.class,timeout30)publicvoidcreateOrder(Orderorder){// 写操作...}}代码组织建议Service 方法要么全有事务要么全没有。不要在同一个 Service 里混合Transactional和非事务的 public 方法避免内部调用陷阱。事务边界放在 Service 层不要在 Controller 层加TransactionalController 拿到的是已脱离事务上下文的代理。**写操作加 **rollbackFor Exception.class读操作加readOnly true。避免在事务方法中调用外部 HTTP/RPC 服务——外部调用耗时长会拉长事务持有数据库连接的时间可能导致连接池耗尽。只 catch 你能处理的异常不确定的让它往上抛。单元测试中验证事务SpringBootTestclassOrderServiceTest{AutowiredprivateOrderServiceorderService;AutowiredprivateOrderMapperorderMapper;TestDisplayName(异常时应回滚订单数据)voidshouldRollbackOnException(){// 使用 assertThrows 确认异常被抛出assertThrows(RuntimeException.class,()-{orderService.createOrderWithException();// 内部会抛异常});// 事务回滚后数据库中应查不到数据ListOrderordersorderMapper.selectAll();assertTrue(orders.isEmpty(),事务应回滚表应为空);}}六、总结Spring 声明式事务失效的根本原因只有一句话事务的方法没有被 Spring 的代理对象拦截到。围绕这条主线本文梳理了 8 个典型的失效场景数据库引擎——MyISAM 天生不支持事务内部调用——this.method()绕过了代理最常见非 public——Spring 默认忽略非公开方法的事务注解异常类型——checked exception 默认不回滚吞异常——catch 后没重新抛出事务管理器无感知多线程——ThreadLocal 绑定的连接无法跨线程传递传播行为——REQUIRES_NEW 等配置导致预期外的独立事务非 Spring Bean——new 出来的对象、static 方法不在 AOP 管辖范围下次遇到事务不回滚的问题对照这份清单逐一排查配合 DEBUG 级别日志通常几分钟就能定位到根因。参考Spring 官方文档 - Transaction ManagementSpring Transaction 源码分析AbstractPlatformTransactionManagerhttps://juejin.cn/post/7482620993283768361https://www.cnblogs.com/haof31/p/19164274Spring 声明式事务原理、使用及失效场景详解_spring声明式事务-CSDN博客
【Spring】声明式 @Transactional 注解失效的 8 大原因及避坑指南
发布时间:2026/6/2 1:14:59
文章目录一、引言二、前置知识Spring 事务原理速览三、八大失效场景逐一拆解3.1 场景一数据库引擎不支持事务3.2 场景二类内部方法调用最常见⭐⭐⭐⭐⭐3.3 场景三非 public 方法3.4 场景四异常类型不匹配3.5 场景五异常被 catch 吞掉常见⭐⭐⭐⭐3.6 场景六多线程环境3.7 场景七事务传播行为配置错误3.8 场景八非 Spring 管理的 Bean 中使用四、快速排查指南五、最佳实践总结推荐的事务注解模板代码组织建议单元测试中验证事务六、总结参考一、引言在 Spring Boot 项目中使用事务非常简单——加个Transactional就搞定。但简单往往意味着容易出错。在实际项目中事务不回滚的 bug 屡见不鲜究其原因大多数是对 Spring 事务的底层实现机制理解不够。Spring 声明式事务的核心依赖是AOP 动态代理。你用Transactional标记的方法Spring 会为它生成一个代理对象在方法执行前后自动开启和提交/回滚事务。理解了这个前提就能推导出一个关键结论只有通过 Spring 代理对象调用的方法事务注解才会生效。本文将从这一核心原理出发梳理 8 大常见失效场景每个场景附带可运行的代码示例和正确的修复方式。二、前置知识Spring 事务原理速览在深入失效场景之前先用一张图理解 Spring 事务的工作链路核心流程如下Spring 启动时扫描Transactional注解为对应 Bean 生成代理对象JDK 动态代理或 CGLIB 代理调用方拿到的是代理对象而不是原始对象代理对象调用TransactionInterceptor它从PlatformTransactionManager获取数据库连接开启事务执行目标方法根据方法执行结果是否抛出异常决定 commit 还是 rollback事务状态Connection绑定在ThreadLocal上这解释了为什么多线程场景下事务会失效。了解这个流程后下面逐一剖析失效场景。三、八大失效场景逐一拆解3.1 场景一数据库引擎不支持事务这是最容易被忽略的低级错误。MySQL 的MyISAM引擎不支持事务如果你的表是 MyISAM加再多Transactional也无济于事。// 错误示例表使用 MyISAM 引擎// CREATE TABLE user (id BIGINT, name VARCHAR(50)) ENGINEMyISAM;ServicepublicclassUserService{TransactionalpublicvoidcreateUser(){userMapper.insert(newUser(1L,张三));// 执行成功inti1/0;// 抛出异常// 预期回滚但 MyISAM 不支持事务第一条数据已持久化}}排查方式执行SHOW TABLE STATUS FROM 数据库名查看Engine列确认是 InnoDB。修复方式将表引擎改为 InnoDB。ALTERTABLEuserENGINEInnoDB;3.2 场景二类内部方法调用最常见⭐⭐⭐⭐⭐这是实际项目中出现频率最高的事务失效场景。直接看代码ServicepublicclassOrderService{// ❌ 错误写法外部调用 createOrder()事务不生效publicvoidcreateOrder(Orderorder){// 非事务方法直接调用同类中的事务方法this.saveOrder(order);// ← this 调用绕过了代理sendNotify(order);}TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 假设这里抛异常数据不会回滚}privatevoidsendNotify(Orderorder){// 发送通知...}}原理分析外部调用orderService.createOrder()时拿到的是 CGLIB 代理对象。createOrder()没有事务注解代理对象直接调用目标对象的方法。而目标对象内部的this.saveOrder()是 Java 原生的方法调用它根本不知道代理对象的存在Transactional自然不生效。自调用机制当类中的方法调用同一个类中的另一个Transactional 方法时事务可能不会生效。这是因为事务注解是通过 AOP 实现的而 Spring 的 AOP 代理机制在这种情况下不会被触发。三种正确的修复方式// ✅ 方式一调用方也标记 Transactional推荐最简单ServicepublicclassOrderService{Transactional// 让外层方法也加入事务管理publicvoidcreateOrder(Orderorder){saveOrder(order);// 此时事务已在外层开启内层复用同一事务sendNotify(order);}publicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 异常会回滚因为事务在外层已开启}privatevoidsendNotify(Orderorder){/* ... */}}// ✅ 方式二将事务方法提取到独立的 Service推荐职责清晰ServicepublicclassOrderService{AutowiredprivateOrderPersistenceServicepersistenceService;// 注入另一个 BeanpublicvoidcreateOrder(Orderorder){persistenceService.saveOrder(order);// ← 跨 Bean 调用走代理事务生效sendNotify(order);}}ServicepublicclassOrderPersistenceService{TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);// 异常会回滚 ✓}}// ✅ 方式三通过 AopContext.currentProxy() 获取代理对象ServicepublicclassOrderService{publicvoidcreateOrder(Orderorder){// 拿到当前类的代理对象再调用事务方法((OrderService)AopContext.currentProxy()).saveOrder(order);sendNotify(order);}TransactionalpublicvoidsaveOrder(Orderorder){orderMapper.insert(order);}}// 需要在启动类或配置类中开启 exposeProxy// EnableAspectJAutoProxy(exposeProxy true)建议优先使用方法二提取独立 Service职责单一、可测试性强。方法一简单但会让外层方法变重适合逻辑不复杂的场景。3.3 场景三非 public 方法Spring 事务注解默认只对 public 方法生效。如果把Transactional放在 private 或 protected 方法上不会有任何事务。ServicepublicclassUserService{// ❌ 错误private 方法上的事务不生效TransactionalprivatevoidsavePrivate(Useruser){userMapper.insert(user);}// ❌ 错误protected 方法上的事务默认也不生效TransactionalprotectedvoidsaveProtected(Useruser){userMapper.insert(user);}// ✅ 正确只能放在 public 方法上Transactionalpublicvoidsave(Useruser){userMapper.insert(user);}}源码依据Spring 的AbstractFallbackTransactionAttributeSource.computeTransactionAttribute()方法中有这样一段逻辑// org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSourceprotectedTransactionAttributecomputeTransactionAttribute(Methodmethod,Class?targetClass){// 如果方法不是 public 的且 allowPublicMethodsOnly() 返回 true默认为 trueif(allowPublicMethodsOnly()!Modifier.isPublic(method.getModifiers())){returnnull;// ← 直接返回 null不应用事务}// ...}Spring 团队的设计理由是非 public 方法不应该暴露为事务边界。如果你确实有特殊需求可以自定义TransactionAttributeSource但强烈不推荐。3.4 场景四异常类型不匹配Transactional默认只对RuntimeException和Error进行回滚。当你抛出一个 checked exception如IOException、自定义受检异常时事务不会回滚。ServicepublicclassUserService{// ❌ 错误抛出 checked exception事务不会回滚TransactionalpublicvoidbatchImport(InputStreaminput)throwsIOException{userMapper.insert(newUser(1L,张三));// 模拟读取异常if(inputnull){thrownewIOException(文件读取失败);// ← checked exception默认不回滚}}// ✅ 正确显式指定 rollbackForTransactional(rollbackForException.class)// 所有异常都回滚publicvoidbatchImportCorrect(InputStreaminput)throwsIOException{userMapper.insert(newUser(2L,李四));if(inputnull){thrownewIOException(文件读取失败);// 这次会回滚 ✓}}// ✅ 更精确的写法只回滚你关心的特定异常Transactional(rollbackFor{IOException.class,BusinessException.class})publicvoidbatchImportPrecise(InputStreaminput)throwsIOException{// ...}}最佳实践项目中统一使用Transactional(rollbackFor Exception.class)或者在全局配置中设置默认回滚策略避免遗漏。3.5 场景五异常被 catch 吞掉常见⭐⭐⭐⭐这是仅次于内部方法调用的高频失效场景。你把异常 catch 住了但没重新抛出事务管理器根本不知道发生了异常自然就不会回滚。ServicepublicclassOrderService{// ❌ 错误catch 后吞掉异常事务不回滚TransactionalpublicvoidcreateOrder(Orderorder){try{orderMapper.insert(order);inti1/0;// 抛出 ArithmeticException// 其他业务逻辑...}catch(Exceptione){log.error(创建订单失败,e);// ← 异常被吞了方法正常返回Spring 提交事务}}}为什么会这样Spring 的事务拦截器在目标方法执行后检查是否有未捕获的异常从方法中抛出。try-catch 兜底后异常不再往外抛拦截器认为方法正常结束于是 commit。三种正确的修复方式Slf4jServicepublicclassOrderService{// ✅ 方式一catch 之后重新抛出运行时异常TransactionalpublicvoidcreateOrderV1(Orderorder){try{orderMapper.insert(order);inti1/0;}catch(Exceptione){log.error(创建订单失败,e);thrownewRuntimeException(创建订单失败,e);// 重新抛出}}// ✅ 方式二手动标记回滚适合需要返回错误码而非抛异常的场景TransactionalpublicvoidcreateOrderV2(Orderorder){try{orderMapper.insert(order);inti1/0;}catch(Exceptione){log.error(创建订单失败,e);// 手动标记当前事务需要回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 此时可以返回业务错误码不需要抛异常}}// ✅ 方式三精确 catch只处理能处理的异常推荐TransactionalpublicvoidcreateOrderV3(Orderorder){try{orderMapper.insert(order);// 调用外部服务允许失败remoteNotifyService.send(order);}catch(RemoteNotifyExceptione){// 通知服务失败不应该回滚订单事务log.warn(通知发送失败但订单正常创建,e);// 不重新抛出 通知失败不影响订单持久化}// RuntimeException 不要 catch让它自然抛出触发回滚}}核心原则只有你明确知道某个异常不应该导致事务回滚时才 catch 它。如果你不确定不要 catch让它自然抛到代理层。3.6 场景六多线程环境Transactional的事务状态通过ThreadLocal绑定到当前线程。当你开启新线程执行数据库操作时新线程里没有事务上下文自然也享受不到事务保护。ServicepublicclassOrderService{AutowiredprivateOrderMapperorderMapper;// ❌ 错误新线程中的方法没有事务TransactionalpublicvoidcreateOrderAsync(Orderorder){orderMapper.insert(order);// 这条在当前线程的事务中// 在子线程中执行数据库操作——事务不传递newThread(()-{orderMapper.insertDetail(order.getDetail());// ← 子线程事务不生效}).start();// 模拟主线程异常inti1/0;// 结果order 会回滚但 orderDetail 已经写入了在子线程的独立连接中}}为什么会这样主线程的事务是把数据库连接Connection存在ThreadLocal中。子线程是一个全新的 Thread它的ThreadLocal是空的所以orderMapper.insertDetail()会从连接池拿一个新的 Connection以非事务模式auto-commit执行。即使主线程回滚了子线程写入的数据早已持久化。更隐蔽的变体使用AsyncSlf4jServicepublicclassOrderService{AutowiredprivateOrderDetailServicedetailService;TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);// Async 方法在独立线程池中执行事务不传递detailService.saveDetailAsync(order.getDetail());inti1/0;// 同样order 回滚detail 不回滚}}ServiceclassOrderDetailService{Async// ← 异步 新线程事务不传递Transactional// ← 这个事务在新的独立线程中与主线程事务无关publicvoidsaveDetailAsync(OrderDetaildetail){detailMapper.insert(detail);}}正确做法// ✅ 方式一如果需要保证原子性不要用多线程TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);orderMapper.insertDetail(order.getDetail());// 同步执行共享同一事务// 两条数据同时成功或同时回滚 ✓}// ✅ 方式二使用分布式事务方案Seata、RocketMQ 事务消息等// ✅ 方式三改成最终一致性方案——主事务提交后通过消息队列异步处理TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);orderMapper.insertDetail(order.getDetail());// 事务提交成功后再发消息// TransactionSynchronizationManager.registerSynchronization(...)}3.7 场景七事务传播行为配置错误Spring 提供了 7 种传播行为Propagation配置不当会让事务看起来失效。ServicepublicclassUserService{AutowiredprivateLogServicelogService;// ❌ 错误外部事务存在时logService 挂起事务再执行插入的日志不回滚TransactionalpublicvoiddeleteUser(LonguserId){userMapper.delete(userId);logService.recordDeleteLog(userId);// 调用另一个事务方法inti1/0;// 异常user 删除会回滚但日志已写入}}ServiceclassLogService{Transactional(propagationPropagation.REQUIRES_NEW)// ← 挂起当前事务新建独立事务publicvoidrecordDeleteLog(LonguserId){logMapper.insert(newOperationLog(userId,DELETE));// 这个事务已提交外部回滚不影响它}}这不是失效而是设计如此。Propagation.REQUIRES_NEW的语义就是新建一个独立的物理事务。你需要根据业务需求选择合适的传播行为传播行为含义适用场景REQUIRED默认有事务则加入没有则新建绝大多数场景REQUIRES_NEW总是新建事务挂起当前事务日志记录、审计等不应因业务回滚而撤销的操作NESTED嵌套事务内部回滚不影响外部部分回滚时需要保存点savepointSUPPORTS有事务就加入没有就以非事务运行查询为主的只读操作NOT_SUPPORTED以非事务方式运行挂起当前事务不需要事务的操作MANDATORY必须在事务中运行否则抛异常强制调用方提供事务NEVER必须在非事务中运行否则抛异常强制无事务执行排查技巧开启 Spring 事务日志观察事务的创建和提交/回滚行为# application.yml 或 application.propertieslogging:level:org.springframework.transaction.interceptor:DEBUGorg.springframework.orm.jpa:DEBUG3.8 场景八非 Spring 管理的 Bean 中使用Transactional只在 Spring 容器管理的 Bean 中生效。以下几种情况注解会静默失效// ❌ 情况1直接 new 出来的对象publicclassSomeController{AutowiredprivateApplicationContextcontext;publicvoidhandle(){// 直接 new不在 Spring 容器中Transactional 无效OrderServiceservicenewOrderService();service.createOrder(order);// 事务不生效}}// ❌ 情况2在 Configuration 类的方法上加 TransactionalConfigurationpublicclassAppConfig{Transactional// ← 无效配置类的 Bean 初始化方法不是业务调用入口publicDataSourcedataSource(){returnnewHikariDataSource();}}// ❌ 情况3工具类的 static 方法publicclassOrderUtils{Transactional// ← 无效static 方法不能被 AOP 代理拦截publicstaticvoidsaveOrder(Orderorder){// ...}}根本原因Spring AOP 代理只能拦截 Spring 容器中 Bean 的实例方法调用。new出来的对象、static 方法、配置类的Bean方法都不满足这个前提。四、快速排查指南遇到事务不生效时按以下清单逐项自查检查项排查命令 / 方法① 数据库引擎SHOW TABLE STATUS确认 InnoDB② 调用链路是否跨 Bean 调用是否用了this.③ 方法修饰符方法是否为public④ 异常类型是否 catch 吞了抛的是否 RuntimeException⑤ 线程环境是否涉及new Thread()或Async⑥ 传播行为Transactional的propagation属性是否正确⑦ Bean 管理类是否由 Spring 容器管理是否是new出来的⑧ 事务日志开启DEBUG日志观察事务行为最直接的诊断手段——开启日志logging:level:org.springframework.transaction.interceptor:DEBUG你会看到类似这样的输出Creating new transaction with name [com.example.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT Opened new Connection [conn1] for JDBC transaction Initiating transaction commit如果加了Transactional但日志里完全没有Creating new transaction说明注解没生效按清单排查。五、最佳实践总结推荐的事务注解模板ServiceSlf4jpublicclassOrderService{/** * 标准事务方法模板 * - rollbackFor Exception.class确保任何异常都回滚 * - timeout防止长事务锁表 * - readOnly true只读操作标记提升性能 */Transactional(rollbackForException.class,timeout30,readOnlytrue)publicOrdergetOrder(LongorderId){returnorderMapper.selectById(orderId);}Transactional(rollbackForException.class,timeout30)publicvoidcreateOrder(Orderorder){// 写操作...}}代码组织建议Service 方法要么全有事务要么全没有。不要在同一个 Service 里混合Transactional和非事务的 public 方法避免内部调用陷阱。事务边界放在 Service 层不要在 Controller 层加TransactionalController 拿到的是已脱离事务上下文的代理。**写操作加 **rollbackFor Exception.class读操作加readOnly true。避免在事务方法中调用外部 HTTP/RPC 服务——外部调用耗时长会拉长事务持有数据库连接的时间可能导致连接池耗尽。只 catch 你能处理的异常不确定的让它往上抛。单元测试中验证事务SpringBootTestclassOrderServiceTest{AutowiredprivateOrderServiceorderService;AutowiredprivateOrderMapperorderMapper;TestDisplayName(异常时应回滚订单数据)voidshouldRollbackOnException(){// 使用 assertThrows 确认异常被抛出assertThrows(RuntimeException.class,()-{orderService.createOrderWithException();// 内部会抛异常});// 事务回滚后数据库中应查不到数据ListOrderordersorderMapper.selectAll();assertTrue(orders.isEmpty(),事务应回滚表应为空);}}六、总结Spring 声明式事务失效的根本原因只有一句话事务的方法没有被 Spring 的代理对象拦截到。围绕这条主线本文梳理了 8 个典型的失效场景数据库引擎——MyISAM 天生不支持事务内部调用——this.method()绕过了代理最常见非 public——Spring 默认忽略非公开方法的事务注解异常类型——checked exception 默认不回滚吞异常——catch 后没重新抛出事务管理器无感知多线程——ThreadLocal 绑定的连接无法跨线程传递传播行为——REQUIRES_NEW 等配置导致预期外的独立事务非 Spring Bean——new 出来的对象、static 方法不在 AOP 管辖范围下次遇到事务不回滚的问题对照这份清单逐一排查配合 DEBUG 级别日志通常几分钟就能定位到根因。参考Spring 官方文档 - Transaction ManagementSpring Transaction 源码分析AbstractPlatformTransactionManagerhttps://juejin.cn/post/7482620993283768361https://www.cnblogs.com/haof31/p/19164274Spring 声明式事务原理、使用及失效场景详解_spring声明式事务-CSDN博客