
1. 项目概述从一次失败的转账说起那天下午我盯着屏幕上那个刺眼的“转账失败但账户余额已扣减”的提示后背一阵发凉。这不是生产环境是我本地跑的一个模拟分布式事务的Demo。逻辑很简单A账户扣款B账户加款中间插入一个一定会抛出异常的操作。理想很丰满扣款成功了异常抛出了但事务没有回滚A账户的钱就这么凭空消失了。那一刻我意识到仅仅会使用Transactional注解是远远不够的如果不搞清楚Spring事务回滚的底层原理尤其是在AOP动态代理的魔法下它究竟是如何工作的那么写出有坑的代码只是时间问题。这次“事故”促使我深入源码去探寻Spring事务管理特别是其回滚机制的核心奥秘。本文将围绕Spring AOP与事务回滚这两个核心关键词拆解Spring是如何将声明式事务与AOP结合并在异常发生时精准触发回滚的。无论你是正在准备面试还是希望提升对Spring框架的深层理解这篇笔记都将带你穿过API的迷雾直抵事务可靠性的设计核心。2. 事务回滚的整体设计与AOP的融合思路2.1 声明式事务的本质AOP的典型应用Spring声明式事务是对AOP面向切面编程理念最经典、最成功的应用之一。我们通常无需编写任何事务控制代码如beginTransaction,commit,rollback仅仅通过一个Transactional注解就能让方法具备事务能力。这背后的魔法正是Spring AOP在起作用。它的核心设计思路是将事务管理开启、提交、回滚这一横切关注点从核心业务逻辑中剥离出来封装成一个独立的“事务增强”Advice并通过动态代理在运行时织入到目标方法周围。当我们调用一个被Transactional标注的Bean的方法时实际上调用的是一个被Spring事务增强代理包装后的对象。这个代理对象负责在方法执行前开启事务在方法正常执行后提交事务在方法抛出异常时决定是否回滚事务。2.2 核心组件协作图景要理解回滚必须先理清几个核心组件在AOP链条上的角色TransactionInterceptor 这是事务增强的核心实现类是一个MethodInterceptor。它包含了事务管理的全部逻辑是决定回滚与否的“大脑”。TransactionAttributeSource 用于解析事务属性例如从Transactional注解中解析出rollbackFor、propagation等属性。它告诉TransactionInterceptor当前方法需要什么样的事务行为。PlatformTransactionManager 事务管理器的统一接口如DataSourceTransactionManager、JpaTransactionManager。TransactionInterceptor最终会委托它来执行物理上的事务开启、提交和回滚操作。ProxyFactory AopProxy Spring AOP用于创建代理对象的工厂。根据目标类是否实现接口决定使用JDK动态代理还是CGLIB代理。它们之间的协作流程可以概括为AOP代理拦截方法调用 → 交给TransactionInterceptor.invoke()→ 解析事务属性 → 通过PlatformTransactionManager管理事务生命周期 → 执行业务方法 → 根据执行结果和事务属性决定提交或回滚。2.3 为什么回滚逻辑必须与异常紧密绑定这是理解事务回滚原理的关键。数据库事务的回滚是对一系列已执行SQL语句的撤销。而触发撤销的唯一信号就是程序运行过程中抛出的异常。Spring无法预知业务逻辑的错误它只能通过捕获异常来判定事务是否失败。因此Spring事务回滚机制的核心就变成了“如何定义异常”和“如何处理异常”这两个问题。Transactional注解中的rollbackFor、noRollbackFor等属性正是为了解决“如何定义异常”而存在的配置项。3. 核心细节解析TransactionInterceptor 的工作流3.1 invoke()方法事务增强的入口一切的故事都从TransactionInterceptor.invoke()方法开始。当我们调用代理方法时AOP框架会最终调用到这个方法。public Object invoke(MethodInvocation invocation) throws Throwable { // 获取目标类、方法等信息 Class? targetClass (invocation.getThis() ! null ? AopUtils.getTargetClass(invocation.getThis()) : null); // 关键在事务中执行调用 return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { Override public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); // 这里会继续调用链最终执行业务方法 } // ... 其他回调方法 }); }invoke方法本身是个外壳它直接委托给了invokeWithinTransaction方法。注意这里传入了一个InvocationCallback它的proceedWithInvocation()封装了原始业务方法的调用即invocation.proceed()。这种设计将事务管理的模板代码和具体的业务调用解耦。3.2 invokeWithinTransaction事务管理的模板方法invokeWithinTransaction是定义在父类TransactionAspectSupport中的核心模板方法。它定义了事务处理的固定流程是理解回滚的重中之重。其方法体骨架清晰地展示了事务的生命周期事务属性获取 首先通过TransactionAttributeSource获取当前方法的事务属性TransactionAttribute。如果属性为空即方法没有Transactional注解则直接执行原始方法不开启事务。事务创建 如果存在事务属性则确定一个PlatformTransactionManager并调用其getTransaction()方法。这个方法至关重要它内部会根据事务传播行为Propagation做出复杂决策例如加入现有事务、挂起当前事务、新建独立事务等最终返回一个代表当前事务状态的TransactionStatus对象。执行业务逻辑 在一个try-catch块中执行回调函数invocation.proceed()即运行我们编写的业务代码。异常处理与回滚决策 在catch块中捕获业务方法抛出的Throwable。这里是回滚逻辑的核心事务提交 如果在try块中业务方法正常返回则在finally块之前的代码路径中执行commit操作。资源清理 在finally块中进行一些事务状态的清理工作。这个try-catch-finally结构是Spring事务可靠性的基石。接下来我们深入最关键的第四步。3.3 回滚决策逻辑completeTransactionAfterThrowing当业务方法抛出异常后控制流会进入catch块并调用completeTransactionAfterThrowing(txInfo, ex)方法。这个方法的名字直译过来就是“在抛出异常后完成事务”其内部逻辑决定了是回滚还是提交。它的决策流程如下protected void completeTransactionAfterThrowing(Nullable TransactionInfo txInfo, Throwable ex) { // 1. 判断当前是否存在事务 if (txInfo ! null txInfo.getTransactionStatus() ! null) { // 2. 核心判断当前抛出的异常是否满足回滚条件 if (txInfo.transactionAttribute ! null txInfo.transactionAttribute.rollbackOn(ex)) { // 2.1 满足条件执行回滚 try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { // 记录回滚失败日志... throw ex2; } catch (RuntimeException | Error ex2) { // 记录回滚失败日志... throw ex2; } } else { // 2.2 不满足回滚条件即使有异常也提交事务 try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { // 记录提交失败日志... throw ex2; } catch (RuntimeException | Error ex2) { // 记录提交失败日志... throw ex2; } } } }关键点在于第2步的rollbackOn(ex)方法。这个方法由RuleBasedTransactionAttribute实现它根据Transactional注解中配置的规则来判断异常是否触发回滚。3.4 rollbackOn规则解析默认行为与自定义rollbackOn的默认规则是Spring事务的一个经典面试题也是很多开发者踩坑的地方。默认规则 如果用户没有通过rollbackFor/noRollbackFor指定任何规则则Spring默认只在遇到RuntimeException运行时异常和Error系统错误时回滚事务。对于受检异常Checked Exception如IOException、SQLException默认是提交事务的。规则覆盖 一旦用户通过rollbackFor指定了任何异常默认规则就失效了。例如Transactional(rollbackFor Exception.class)意味着所有Exception及其子类包括受检异常都会触发回滚。noRollbackFor拥有更高的优先级可以排除特定的异常即使它符合rollbackFor的规则。这个设计哲学源于一个普遍认知RuntimeException通常代表程序不可预期的错误如空指针、数组越界、数据库连接失败事务应该回滚。而受检异常往往代表一种可预期的业务异常如“用户余额不足”、“库存不够”业务上可能需要捕获并处理不一定要回滚整个事务。当然这个默认设定可以根据业务需求覆盖。实操心得 我强烈建议在项目开始时就明确事务的异常回滚策略。对于大多数业务服务层方法使用Transactional(rollbackFor Exception.class)是更安全的选择可以避免因为漏掉某个受检异常而导致数据不一致。但也要注意这可能会将一些本应提交的业务异常也回滚掉需要结合具体业务逻辑权衡。4. 实操过程从注解到回滚的完整链路追踪4.1 环境准备与一个可调试的案例为了亲眼看到整个流程我们创建一个最简单的Spring Boot应用。依赖 只需要spring-boot-starter-data-jpa或spring-boot-starter-jdbc和spring-boot-starter-aop。Spring Boot会自动配置事务管理器。数据源 配置一个H2内存数据库即可方便测试。实体与ServiceEntity public class Account { Id private Long id; private String name; private BigDecimal balance; // getters and setters } Service public class TransferService { Autowired private AccountRepository repository; Transactional(rollbackFor Exception.class) // 明确指定回滚规则 public void transfer(Long fromId, Long toId, BigDecimal amount) { Account from repository.findById(fromId).orElseThrow(); Account to repository.findById(toId).orElseThrow(); from.setBalance(from.getBalance().subtract(amount)); repository.save(from); // 模拟一个业务异常 if (amount.compareTo(new BigDecimal(1000)) 0) { throw new RuntimeException(转账金额超过1000拒绝操作); } to.setBalance(to.getBalance().add(amount)); repository.save(to); } }这个transfer方法模拟了转账操作当金额超过1000时会抛出RuntimeException。4.2 使用调试器追踪调用栈在IDE中在TransferService.transfer方法内和TransactionInterceptor.invoke方法内打上断点。启动应用调用transferService.transfer(1L, 2L, new BigDecimal(1500))。程序首先会停在TransactionInterceptor.invoke的断点。这时观察invocation变量可以看到其target是TransferService的实例但method是被代理增强后的方法。步入invokeWithinTransaction。在此处你可以看到TransactionAttributeSource如何解析出我们注解上配置的rollbackFor Exception.class属性生成了一个RuleBasedTransactionAttribute对象。继续执行会调用transactionManager.getTransaction()由于是第一个事务这里会创建一个新的事务连接。步入invocation.proceed()这时程序会跳转到我们自己的transfer业务方法中。在业务方法中执行扣款、判断金额、抛出RuntimeException。异常抛出后控制权立刻返回到invokeWithinTransaction的catch块中。此时单步进入completeTransactionAfterThrowing。观察传入的异常ex正是我们抛出的RuntimeException。关键步骤步入rollbackOn(ex)方法。因为我们在注解中配置了rollbackFor Exception.class而RuntimeException是Exception的子类所以这里会返回true。由于rollbackOn返回true程序会执行transactionManager.rollback()。步入这个方法你会看到它调用底层数据库连接如JDBCConnection的rollback()方法并清理线程绑定的事务资源。最后异常被继续向上抛出被Controller层或测试用例捕获。检查数据库你会发现两个账户的余额都没有发生变化事务成功回滚。4.3 修改异常类型观察不同行为我们将transfer方法中的异常改为一个受检异常并修改注解来观察不同配置下的行为。案例A使用默认注解即TransactionalTransactional // 默认只对RuntimeException和Error回滚 public void transferChecked(Long fromId, Long toId, BigDecimal amount) throws IOException { // ... 同样的扣款逻辑 if (amount.compareTo(new BigDecimal(1000)) 0) { throw new IOException(金额过大模拟IO异常); // 受检异常 } // ... 同样的加款逻辑 }在这种情况下即使抛出了IOException因为默认规则不回滚受检异常事务会被提交扣款操作生效加款操作未执行数据不一致。这就是文章开头我遇到的坑。案例B明确指定rollbackForTransactional(rollbackFor IOException.class) public void transferChecked(Long fromId, Long toId, BigDecimal amount) throws IOException { // ... 逻辑同上 }此时rollbackOn方法会识别到IOException匹配了rollbackFor的规则从而触发回滚保证了数据一致性。通过这个调试过程你可以清晰地看到从注解配置到AOP拦截再到异常匹配和最终调用PlatformTransactionManager的完整决策链路。这比任何文档说明都更加深刻。5. 进阶传播行为与回滚的复杂交互事务传播行为Propagation定义了多个事务方法相互调用时事务应该如何传播。它与回滚机制有着复杂的交互是高级使用的难点。5.1 REQUIRED默认与回滚REQUIRED是默认的传播行为。如果当前没有事务就新建一个如果当前已存在事务就加入该事务。回滚影响在REQUIRED模式下内层方法加入外层方法的事务形成一个物理上的大事务。只要这个大事务中的任何一点抛出了满足回滚条件的异常并且这个异常没有被内部捕获处理整个大事务都将回滚。这就是“一荣俱荣一损俱损”。Service public class OuterService { Autowired private InnerService innerService; Transactional public void outerMethod() { // 操作A try { innerService.innerMethod(); // 内层方法抛出异常 } catch (Exception e) { // 捕获了异常 System.out.println(捕获了内部异常但事务呢); } // 操作B } } Service public class InnerService { Transactional(propagation Propagation.REQUIRED) public void innerMethod() { throw new RuntimeException(内部异常); } }结果分析innerMethod抛出的RuntimeException被outerMethod捕获了。异常没有继续向上抛给TransactionInterceptor因此事务不会触发回滚。操作A和操作B都会被提交。这常常是导致业务数据部分生效的隐蔽Bug。注意事项 在事务方法内部调用另一个事务方法时如果内层方法可能抛出异常且你不希望影响外层事务必须在外层方法中进行捕获和处理。仅仅在内层方法自己catch是没用的因为异常在离开内层方法代理时就已经被TransactionInterceptor处理了。5.2 REQUIRES_NEW 与回滚REQUIRES_NEW总是会启动一个新的事务。如果当前存在事务则将其挂起。回滚影响内外层事务完全独立。内层事务的回滚不会影响外层事务。外层事务的回滚也不会影响已提交的内层事务。Service public class OuterService { Autowired private InnerService innerService; Transactional public void outerMethod() { // 操作A (属于外层事务Tx1) innerService.innerMethod(); // 开启一个新事务Tx2 // 操作B (属于外层事务Tx1) throw new RuntimeException(外层异常); // Tx1回滚 } } Service public class InnerService { Transactional(propagation Propagation.REQUIRES_NEW) public void innerMethod() { // 操作C (属于内层独立事务Tx2) // 此操作会成功提交不受外层Tx1回滚影响 } }结果分析操作C在独立事务Tx2中会先提交。随后outerMethod抛出异常导致外层事务Tx1回滚但Tx1的回滚无法撤销已经提交的Tx2。因此最终结果是操作C生效操作A和操作B被回滚。这种特性常用于记录日志、发送审计消息等操作这些操作的成功与否不应影响核心业务事务。5.3 NESTED 与回滚NESTED在现有事务中启动一个“嵌套事务”。它是外部事务的一个保存点Savepoint。这是JDBC驱动支持的特性。回滚影响内层嵌套事务的回滚只会回滚到保存点不影响外部事务已执行的操作。但外部事务的回滚会导致所有嵌套事务一起回滚。// 假设数据库和事务管理器支持保存点 Transactional public void outerMethod() { // 操作A try { innerMethod(); // NESTED事务设置保存点 } catch (Exception e) { // 只回滚innerMethod中的操作 } // 操作B } Transactional(propagation Propagation.NESTED) public void innerMethod() { // 操作C throw new RuntimeException(); }结果分析innerMethod回滚仅操作C失效。outerMethod继续执行操作B并且最终可以提交整个事务包含操作A和操作B。这提供了比REQUIRES_NEW更灵活的、部分回滚的能力但需要底层资源如JDBC连接的支持。6. 常见问题排查与深度避坑指南6.1 问题1为什么我的Transactional注解失效了这是最高频的问题。失效通常意味着AOP代理没有生效事务根本没有开启。请按以下清单排查排查点原因分析解决方案方法修饰符Transactional注解标注在private、protected或static方法上。Spring AOP默认使用动态代理无法代理这些方法。确保方法为public。自调用问题在同一个Bean内部方法A调用方法BB有Transactional。由于调用走的是this引用而非代理对象B的事务增强不会生效。1. 将方法B移到另一个Bean中。2. 注入自身的代理Autowired private MyService self;通过self.methodB()调用。3. 使用AspectJ编译时/加载时织入LTW模式。异常被内部捕获事务方法内部try-catch了异常但没有重新抛出。TransactionInterceptor捕获不到异常自然不会触发回滚。在catch块中要么重新抛出异常throw e;要么手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。数据库引擎不支持使用的MySQL表引擎是MyISAM它不支持事务。将表引擎改为InnoDB。未启用事务管理在非Spring Boot项目中可能忘记配置EnableTransactionManagement。在配置类上添加EnableTransactionManagement注解。6.2 问题2回滚了但部分数据好像还是变了这可能涉及事务隔离级别和可见性问题。非事务操作混杂 在事务方法中混杂了非事务性的数据操作。例如在Transactional方法中你使用了JdbcTemplate直接执行了一条SQL但JdbcTemplate的每次操作默认是自动提交的除非你将其也纳入事务管理这会导致该SQL立即生效不受后续事务回滚影响。缓存与数据库不一致 如果使用了Hibernate、MyBatis等ORM框架的一级/二级缓存或者应用层缓存如Redis事务回滚只会回滚数据库。缓存中的数据可能已被更新且未失效导致后续查询读到脏数据。需要在事务成功提交或回滚后手动清理相关缓存。业务逻辑中的非数据库操作 例如在事务方法中调用了发送消息MQ、调用外部API、写本地文件等操作。这些操作不受数据库事务管理一旦执行就无法“回滚”。这是分布式事务的范畴需要通过消息事务表、最终一致性方案等来解决。6.3 问题3事务提交太慢或出现死锁这通常与事务作用域过大有关。长事务问题 一个Transactional注解标注在包含复杂业务逻辑、远程调用、文件IO等耗时操作的方法上导致数据库连接被长时间占用。这会严重影响数据库连接池性能和并发能力。优化建议 遵循“事务最小化”原则。将不必要的操作如数据准备、参数校验、非核心业务调用移到事务方法外部。确保事务内部只包含必须原子执行的数据库操作。死锁 多个事务以不同的顺序访问和更新相同的资源数据库行、表可能导致死锁。Spring事务默认超时时间是-1无超时死锁会一直等待。优化建议为Transactional设置合理的超时时间Transactional(timeout 30)。在业务代码中尽量以固定的顺序访问资源。使用数据库的锁机制如SELECT ... FOR UPDATE时要谨慎避免循环等待。6.4 一个关于Transactional on类级别的陷阱将Transactional注解标注在类上意味着该类的所有public方法都继承了该事务属性。但这会引入一个隐蔽问题Service Transactional(readOnly true) // 类级别默认只读 public class UserService { public User getUser(Long id) { return repository.findById(id).orElse(null); } public void updateUser(User user) { // 这个方法实际上也是 readOnly true repository.save(user); } }updateUser方法从类级别继承了readOnly true属性。大多数数据库在只读事务中执行写操作会直接抛出异常如MySQL的The MySQL server is running with the --read-only option错误。更糟糕的是有些配置或驱动可能不会立即报错导致数据写入异常或丢失。最佳实践 优先将Transactional注解用在方法级别进行精确控制。如果类级别确有需要务必在需要进行写操作的方法上显式覆写属性Transactional(readOnly false)。通过对Spring AOP事务回滚原理的层层拆解我们从一次“转账事故”出发穿越了动态代理、拦截器、属性源、事务管理器组成的完整链条最终理解了异常如何触发回滚这一核心机制。掌握这些原理不仅能让你在面试中游刃有余更能让你在复杂业务开发中自信地设计出正确、可靠的事务边界避免数据不一致的幽灵。记住事务不是银弹清晰的业务边界和对底层机制的理解才是构建稳健系统的基石。