资讯动态

Spring @Transactional 事务失效的七大场景与排查修复指南

发布时间:2026/10/14 4:15:01 来源:尧图企业网站定制
1. 事务失效的底层逻辑与排查思路Transactional这个注解几乎每个做 Java 后端的人都用过。加在方法上方法里抛异常就回滚看起来简单得不能再简单。但真正在生产环境里踩过坑的人都知道这玩意儿失效的方式五花八门而且失效的时候往往不报错、不抛异常数据该回滚的没回滚该入库的没入库排查起来让人头皮发麻。我自己在几个项目里都遇到过事务失效的问题最典型的一次是批量导入数据明明中间有一条记录插入失败了但前面的记录还是进了库导致数据对不上。当时查了半天最后发现是方法内部调用导致代理没生效。从那以后我开始系统性地梳理Transactional失效的各种场景今天就把这些经验完整地分享出来。这篇文章适合所有使用 Spring 框架做后端开发的工程师不管你是刚接触事务的新手还是已经用过几年但没深究过原理的老手都能从中找到自己需要的东西。我会从底层原理讲起把七种最常见的失效原因逐一拆解每种都配上可复现的代码示例和修复方案最后再给出一份排查清单方便你在遇到问题时快速定位。1.1 先搞懂 Spring 事务是怎么生效的要理解事务为什么会失效必须先搞清楚它正常工作时是怎么运作的。Spring 的声明式事务本质上是通过 AOP 动态代理实现的。当你在一个 Bean 的方法上标注TransactionalSpring 容器在初始化这个 Bean 的时候会判断它是否需要事务增强。如果需要就会为这个 Bean 生成一个代理对象你从容器里拿到的实际上是代理对象而不是原始对象。代理对象在调用目标方法之前会先开启事务方法执行完成后根据执行结果决定是提交还是回滚。这个逻辑封装在TransactionInterceptor里它实现了MethodInterceptor接口在invoke方法中完成了事务的完整生命周期管理。关键点在于只有通过代理对象调用方法事务才会生效。如果你绕过了代理对象直接调用原始对象的方法那事务增强的逻辑根本不会执行Transactional就形同虚设。这是理解所有失效场景的基础。另一个关键点是事务的传播行为。Spring 默认的传播级别是PROPAGATION_REQUIRED意思是如果当前没有事务就新建一个如果有就加入当前事务。这个默认值在大多数场景下没问题但在某些嵌套调用的场景下可能会导致你以为会回滚的操作实际上没有回滚。还有一个容易被忽略的点是回滚规则。Spring 默认只在遇到RuntimeException和Error时回滚检查型异常Exception及其子类中非运行时异常的部分默认不回滚。这个设计初看起来有点反直觉但仔细想想也有道理检查型异常通常表示可恢复的业务异常框架认为你应该在代码里处理掉而不是直接回滚。提示理解代理机制和回滚规则是排查事务失效问题的两把钥匙。遇到事务不生效时先问自己两个问题调用是否经过了代理抛出的异常是否在回滚规则范围内1.2 排查事务失效的通用思路在实际排查事务问题时我一般按照以下顺序来定位确认代理是否生效在方法里打印this.getClass().getName()看看是不是代理类的名字。如果看到的是原始类名说明代理没生效。确认异常是否被吞掉检查方法内部是否有 try-catch 把异常吃掉了导致 Spring 感知不到异常。确认异常类型是否匹配回滚规则如果是检查型异常默认不回滚需要显式配置rollbackFor。确认数据库引擎是否支持事务MySQL 的 MyISAM 引擎不支持事务建表时如果没指定 InnoDB事务也不会生效。确认方法是否是 public 的Spring 的声明式事务默认只对 public 方法生效。这个排查顺序基本覆盖了最常见的几种情况后面我会逐一展开讲每种场景的细节和修复方法。2. 七种典型失效场景逐一拆解2.1 同类内部方法调用导致代理失效这是最常见、也是最容易踩的一种坑。看下面这段代码Service public class OrderService { Autowired private OrderMapper orderMapper; public void createOrder(Order order) { // 这里直接调用同类的方法代理不生效 saveOrder(order); } Transactional public void saveOrder(Order order) { orderMapper.insert(order); // 模拟异常 throw new RuntimeException(插入失败); } }在这个例子中createOrder方法调用了同类中的saveOrder方法。虽然saveOrder标注了Transactional但调用是通过this进行的也就是原始对象而不是代理对象。代理对象根本没有机会介入事务自然不会生效。为什么会这样因为 Spring 的代理对象是在原始对象外面包了一层外部调用先经过代理代理再调用原始对象的方法。但一旦进入了原始对象的方法内部后续的调用就都是原始对象自己的行为了代理层已经看不见了。修复方案有三种第一种是把saveOrder方法挪到另一个 Bean 里通过注入的方式调用。这是最推荐的做法职责也更清晰。第二种是通过AopContext.currentProxy()获取当前代理对象再调用但需要开启exposeProxy配置EnableAspectJAutoProxy(exposeProxy true)然后在代码里这样调用((OrderService) AopContext.currentProxy()).saveOrder(order);第三种是注入自己Autowired private OrderService self; public void createOrder(Order order) { self.saveOrder(order); }注意注入自己的方式在 Spring Boot 2.6 之后可能会报循环依赖的错误需要额外配置spring.main.allow-circular-referencestrue所以更推荐第一种方案。2.2 方法不是 public 导致事务不生效Spring 的声明式事务默认只对 public 方法生效。如果你把Transactional加在 protected、private 或者包级私有的方法上事务不会生效而且不会报任何错误静默失效。Service public class UserService { Transactional protected void updateUser(User user) { // 事务不会生效 userMapper.update(user); } }为什么只支持 public因为 Spring 的 AOP 代理是基于 JDK 动态代理或 CGLIB 实现的。JDK 动态代理只能代理接口的 public 方法CGLIB 虽然可以代理 protected 方法但 Spring 的事务拦截器默认只对 public 方法进行拦截。这是框架的设计约定不是 bug。修复方案很简单把方法改成 public。如果确实需要非 public 方法有事务可以考虑使用编程式事务通过TransactionTemplate手动控制。Autowired private TransactionTemplate transactionTemplate; public void updateUser(User user) { transactionTemplate.execute(status - { userMapper.update(user); return null; }); }2.3 异常被 catch 吞掉导致事务不回滚这个场景也很常见。方法内部用 try-catch 捕获了异常但没有重新抛出Spring 的事务拦截器感知不到异常就会认为方法正常执行完成从而提交事务。Transactional public void batchInsert(ListOrder orders) { for (Order order : orders) { try { orderMapper.insert(order); } catch (Exception e) { // 异常被吞掉事务不会回滚 log.error(插入失败, e); } } }这段代码的意图可能是跳过失败的记录继续插入其他记录但问题是如果外层还有事务异常被吞掉后外层事务不会回滚已经插入的数据也不会撤销。修复方案取决于你的业务需求如果希望任何一条失败就整体回滚那就不要 catch或者 catch 后重新抛出Transactional public void batchInsert(ListOrder orders) { for (Order order : orders) { try { orderMapper.insert(order); } catch (Exception e) { log.error(插入失败, e); throw new RuntimeException(批量插入失败, e); } } }如果希望部分成功部分失败那就不要加Transactional让每条记录独立提交。或者使用REQUIRES_NEW传播级别让每条记录在独立事务中执行。实操心得我见过很多人在 catch 块里只打日志不抛出然后疑惑为什么事务没回滚。记住一个原则只要你想让事务回滚就必须让异常传播到事务拦截器能感知到的地方。2.4 异常类型不匹配导致不回滚Spring 默认只在遇到RuntimeException和Error时回滚。如果你抛出的是检查型异常比如IOException、SQLException默认不会回滚。Transactional public void importData(String filePath) throws IOException { // 插入数据 dataMapper.insert(data); // 抛出检查型异常默认不回滚 throw new IOException(文件读取失败); }修复方案在Transactional注解上显式指定rollbackForTransactional(rollbackFor Exception.class) public void importData(String filePath) throws IOException { dataMapper.insert(data); throw new IOException(文件读取失败); }我个人的习惯是所有Transactional注解都加上rollbackFor Exception.class这样不管抛出什么异常都会回滚避免因为异常类型不匹配导致的问题。虽然这样会失去一些精细控制的能力但在大多数业务场景下这反而是更安全的选择。2.5 传播行为设置不当导致事务失效Spring 支持七种传播行为不同的传播行为在不同场景下表现差异很大。如果设置不当可能会导致事务看起来失效了。最常见的坑是NOT_SUPPORTED和NEVERNOT_SUPPORTED以非事务方式执行如果当前存在事务就把当前事务挂起。NEVER以非事务方式执行如果当前存在事务就抛出异常。如果你在一个事务方法里调用了标注NOT_SUPPORTED的方法那个方法里的操作不会受到事务保护。另一个常见的坑是REQUIRES_NEW和NESTED的混淆REQUIRES_NEW每次都新建一个独立的事务外层事务回滚不影响内层事务。NESTED如果当前有事务就在嵌套事务中执行外层回滚会导致内层也回滚但内层回滚不影响外层。Transactional(propagation Propagation.REQUIRES_NEW) public void saveLog(Log log) { // 这个方法的事务是独立的外层回滚不影响这里 logMapper.insert(log); }注意REQUIRES_NEW会新开一个数据库连接如果外层事务持有锁内层事务又需要同一把锁就会造成死锁。使用时要特别小心。2.6 数据库引擎不支持事务这个问题跟 Spring 没关系是数据库层面的。MySQL 的 MyISAM 引擎不支持事务如果你建表时没有指定 InnoDB或者数据库默认引擎是 MyISAM那事务也不会生效。-- 查看表的引擎 SHOW TABLE STATUS LIKE order_table; -- 建表时指定引擎 CREATE TABLE order_table ( id BIGINT PRIMARY KEY, ... ) ENGINEInnoDB;修复方案确保表使用 InnoDB 引擎。可以通过修改 MySQL 配置文件把默认引擎设置为 InnoDB[mysqld] default-storage-engineInnoDB这个问题在早期 MySQL 版本中比较常见现在大多数项目默认都是 InnoDB但如果你接手了一个老项目还是值得检查一下。2.7 多数据源未正确配置事务管理器在多个数据源的场景下每个数据源需要独立的事务管理器。如果你只配置了一个事务管理器或者Transactional没有指定使用哪个事务管理器就可能导致事务失效。Configuration public class DataSourceConfig { Bean public DataSourceTransactionManager orderTransactionManager(DataSource orderDataSource) { return new DataSourceTransactionManager(orderDataSource); } Bean public DataSourceTransactionManager userTransactionManager(DataSource userDataSource) { return new DataSourceTransactionManager(userDataSource); } }使用时需要指定事务管理器Transactional(transactionManager orderTransactionManager) public void createOrder(Order order) { orderMapper.insert(order); }修复方案为每个数据源配置独立的事务管理器并在Transactional中显式指定。如果使用了分布式事务框架还需要额外配置协调器。3. 可复现的实操验证与修复方案3.1 搭建最小复现环境为了验证上面说的各种场景我搭建了一个最小的 Spring Boot 项目。依赖很简单dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId /dependency数据库配置spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver建表语句CREATE TABLE order_table ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL, amount decimal(10,2) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 逐个场景验证场景一验证同类调用Service public class OrderService { Autowired private JdbcTemplate jdbcTemplate; public void createOrder(String orderNo, BigDecimal amount) { // 直接调用同类方法 saveOrder(orderNo, amount); } Transactional public void saveOrder(String orderNo, BigDecimal amount) { jdbcTemplate.update(INSERT INTO order_table(order_no, amount) VALUES(?, ?), orderNo, amount); throw new RuntimeException(模拟异常); } }调用createOrder后查询数据库会发现记录已经插入事务没有回滚。这就是代理失效的典型表现。修复后验证Service public class OrderService { Autowired private JdbcTemplate jdbcTemplate; Autowired private OrderService self; public void createOrder(String orderNo, BigDecimal amount) { self.saveOrder(orderNo, amount); } Transactional(rollbackFor Exception.class) public void saveOrder(String orderNo, BigDecimal amount) { jdbcTemplate.update(INSERT INTO order_table(order_no, amount) VALUES(?, ?), orderNo, amount); throw new RuntimeException(模拟异常); } }再次调用数据库中没有记录事务回滚成功。场景二验证非 public 方法Transactional protected void updateOrder(Long id, BigDecimal amount) { jdbcTemplate.update(UPDATE order_table SET amount ? WHERE id ?, amount, id); throw new RuntimeException(模拟异常); }调用后金额被更新事务没有回滚。改成 public 后事务正常回滚。场景三验证异常被吞掉Transactional(rollbackFor Exception.class) public void batchInsert(ListString orderNos) { for (String orderNo : orderNos) { try { jdbcTemplate.update(INSERT INTO order_table(order_no, amount) VALUES(?, ?), orderNo, BigDecimal.TEN); if (fail.equals(orderNo)) { throw new RuntimeException(模拟失败); } } catch (Exception e) { log.error(插入失败: {}, orderNo, e); // 异常被吞掉不抛出 } } }调用后所有记录都插入了包括失败的那条之后的记录。修复方式是 catch 后重新抛出或者使用REQUIRES_NEW让每条记录独立事务。3.3 验证事务是否生效的通用方法我常用的一个技巧是在方法里打印当前线程是否在事务中Transactional public void saveOrder(String orderNo) { boolean actualTransactionActive TransactionSynchronizationManager.isActualTransactionActive(); log.info(当前事务是否激活: {}, actualTransactionActive); // ... }如果打印出来是false说明事务没有生效。这个方法简单直接比看日志、查数据库快得多。另一个方法是注入PlatformTransactionManager查看当前事务状态Autowired private PlatformTransactionManager transactionManager; public void checkTransaction() { TransactionStatus status transactionManager.getTransaction(new DefaultTransactionDefinition()); log.info(事务是否为新事务: {}, status.isNewTransaction()); transactionManager.commit(status); }实操心得在开发阶段我习惯在关键的事务方法里加上事务状态日志这样一旦事务没生效第一时间就能发现不用等到数据出问题才去排查。4. 常见问题速查与避坑指南4.1 事务失效排查速查表问题现象可能原因排查方法修复方案异常抛出但数据未回滚异常被 catch 吞掉检查方法内是否有 try-catchcatch 后重新抛出异常异常抛出但数据未回滚异常类型是检查型异常查看异常类是否继承 RuntimeException添加 rollbackFor Exception.class事务完全不生效同类内部方法调用打印 this.getClass() 看是否是代理类拆分到不同 Bean 或注入自己事务完全不生效方法不是 public检查方法修饰符改为 public事务完全不生效数据库引擎不支持SHOW TABLE STATUS 查看引擎改为 InnoDB事务完全不生效多数据源未指定事务管理器检查是否配置了多个事务管理器指定 transactionManager部分回滚部分不回滚传播行为设置不当检查 propagation 配置根据业务调整传播级别4.2 几个容易忽略的细节第一个细节Transactional加在接口上还是实现类上Spring 官方推荐加在实现类上而不是接口上。虽然加在接口上在大多数情况下也能工作但如果使用 CGLIB 代理接口上的注解可能不会被识别。我个人的习惯是统一加在实现类的 public 方法上。第二个细节事务方法里不要做耗时操作事务开启后会占用数据库连接如果方法里有远程调用、文件读写等耗时操作会导致连接被长时间占用影响系统吞吐量。我见过一个项目在事务方法里调用了第三方接口结果第三方接口超时导致数据库连接池被耗尽整个系统雪崩。第三个细节Transactional和Async一起用要小心如果同一个方法上同时标注了Transactional和Async事务会在异步线程中执行但异步线程和调用线程的事务上下文是隔离的。如果调用线程有事务异步方法不会加入而是新开一个事务。这可能导致数据不一致。第四个细节自调用时REQUIRES_NEW也会失效即使是REQUIRES_NEW传播级别如果是同类内部调用同样不会生效因为代理根本没介入。这个坑比普通的事务失效更隐蔽因为你会以为REQUIRES_NEW会新开事务但实际上连事务都没有。4.3 我踩过的几个真实坑坑一批量插入时部分失败导致数据不一致有一次做批量导入方法上加了Transactional但循环里每条记录都 try-catch 了结果失败的记录被跳过成功的记录全部入库。业务方要求的是要么全成功要么全失败最后改成 catch 后收集异常循环结束后统一抛出。坑二多数据源下事务管理器没指定项目里有两个数据源一个主库一个从库。写操作在主库但Transactional没指定事务管理器默认用了从库的事务管理器导致写操作的事务根本没生效。排查了半天才发现是事务管理器用错了。坑三MySQL 引擎是 MyISAM接手一个老项目发现事务怎么都不生效。查了半天代码没问题最后SHOW TABLE STATUS一看表引擎是 MyISAM。改成 InnoDB 后问题解决。这个坑现在比较少见但老项目里还是有可能遇到。坑四异常被全局异常处理器吞掉项目里配置了全局异常处理器方法里抛出的异常被处理器捕获并返回了统一错误响应但事务拦截器感知不到异常因为异常已经被处理掉了。这种情况需要确保异常在到达全局处理器之前已经被事务拦截器处理或者使用编程式事务手动回滚。4.4 事务最佳实践清单根据我这些年的经验总结了几条事务使用的最佳实践所有Transactional都加上rollbackFor Exception.class避免异常类型不匹配的问题。事务方法尽量保持简短不要在事务里做远程调用、文件操作等耗时动作。同类内部调用的事务方法拆分到不同的 Bean这是最稳妥的方案。多数据源场景下显式指定事务管理器不要依赖默认值。在开发阶段加上事务状态日志及时发现事务未生效的问题。批量操作根据业务需求选择传播行为需要部分成功就用REQUIRES_NEW需要全部回滚就用默认的REQUIRED。定期检查数据库表的引擎确保是 InnoDB。事务这个东西说简单也简单加个注解就完事说复杂也复杂各种失效场景防不胜防。我的建议是不要等到出了问题才去排查而是在写代码的时候就养成好习惯把上面这些坑都避开。毕竟数据一致性是后端开发的生命线事务失效导致的数据问题修复起来往往比预防要麻烦得多。最后再分享一个小技巧如果你不确定事务是否生效可以在方法执行前后各查一次数据库对比数据是否一致。这个方法虽然笨但最直接有效。另外养成看 Spring 启动日志的习惯如果代理创建失败或者有循环依赖启动日志里通常会有警告信息提前发现能省不少事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑