如果这篇文章对你有帮助,欢迎关注我的CSDN账号「来福猿」, 有问题可以在评论区留言,我会一一回复。一、从一个问题说起
在 Spring 项目中,我们通常只需要在 Service 方法上添加一个@Transactional注解,方法执行过程中对数据库的多次操作就会被纳入同一个事务:方法正常返回时事务提交,方法抛出异常时事务回滚。很多开发者在日常工作中已经习惯了这种写法,但如果进一步追问:注解到底是如何生效的?提交和回滚究竟发生在哪一行代码?为什么同类内部调用会失效?要回答这些问题,就必须深入到 Spring 事务的源码实现中去。
本文以 Spring Framework 的声明式事务为核心,沿着@Transactional注解的生效链路,逐步分析事务代理的创建、事务拦截器的执行、事务状态的保存与还原,以及最终commit和rollback的触发过程。阅读本文前,建议对 Spring AOP 和 JDBC 事务的基本概念有一定了解。
二、@Transactional 如何变成一段可执行逻辑
单靠一个注解本身并不能完成事务控制,注解只是元数据。真正让事务生效的,是 Spring 在容器启动阶段为带有该注解的 Bean 创建了代理对象。这个代理对象会拦截目标方法的调用,在方法执行前后插入事务管理的逻辑。
整个声明式事务可以拆成两条主线:
- 启动阶段:解析配置、注册切面、创建代理,核心角色是
BeanFactoryTransactionAttributeSourceAdvisor和AnnotationTransactionAttributeSource。 - 运行阶段:代理拦截方法调用,核心角色是
TransactionInterceptor和TransactionAspectSupport。
从这个角度看,@Transactional本质上是 Spring AOP 的一个典型应用。理解这一点,是理解后面所有源码流程的前提。
三、事务代理是如何被创建的
Spring Boot 默认使用ProxyTransactionManagementConfiguration注册事务管理相关的 Bean。该配置类会创建以下几个关键对象,它们共同构成了声明式事务的基础设施。
3.1 事务属性源 TransactionAttributeSource
其核心实现是AnnotationTransactionAttributeSource。它的作用是从类、接口、方法上读取@Transactional注解,并把这些注解信息转换成TransactionAttribute,其中保存了事务传播行为、隔离级别、超时时间、只读标志以及回滚规则等属性。
当 Spring 创建代理时,会通过computeTransactionAttribute方法依次查找方法上的注解、目标类上的注解,以及接口或父类上的注解。保证具体方法上的配置优先于类级别配置,这是大家在同一个类中覆盖默认事务设置的实现基础。
3.2 事务 advisor 与代理的关系
BeanFactoryTransactionAttributeSourceAdvisor是一个 PointcutAdvisor,它通过TransactionAttributeSourcePointcut判断某个类或方法是否需要被事务代理。判断的依据就是上文提到的TransactionAttributeSource能否在该类或方法上找到事务属性。
在 Bean 的初始化阶段,AbstractAutoProxyCreator会调用getAdvicesAndAdvisorsForBean检查每个 Bean。如果 Bean 中存在方法匹配到该 advisor,就为该 Bean 创建代理对象。最终注入到 Controller 或其它调用方手里的 Service,已经是增强后的代理实例。
下面用一张图概括代理创建与拦截的关系:
flowchart LR A[目标 Bean] --> B[TransactionAttributeSourcePointcut 匹配点] B --> C{是否命中 @Transactional} C -->|是| D[创建 JDK 或 CGLIB 代理] C -->|否| E[不创建事务代理] D --> F[TransactionInterceptor 织入代理] F --> G[方法调用时进入事务拦截链]四、事务拦截器:事务逻辑的真正入口
方法调用时,代理不会被直接交给目标对象,而是先进入TransactionInterceptor。它的invoke方法继承自TransactionAspectSupport,是整个事务控制流程的中枢。
简化后的调用结构如下:
public Object invoke(MethodInvocation invocation) throws Throwable { Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis()); return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { @Override public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); } }); }核心逻辑全部汇聚在invokeWithinTransaction方法中。它主要完成四件事:
- 根据目标方法和目标类解析出
TransactionAttribute。 - 根据事务属性获取对应的
TransactionManager。 - 调用事务管理器开启事务,得到
TransactionInfo并绑定到当前线程。 - 执行目标方法,根据执行结果决定提交或回滚,最后恢复线程上下文。
这个方法的骨架可以概括为下面这段逻辑:
Object retVal; try { // 1. 解析事务属性 TransactionAttribute txAttr = tas.getTransactionAttribute(method, targetClass); // 2. 获取事务管理器 TransactionManager tm = determineTransactionManager(txAttr); // 3. 创建事务信息 TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); retVal = null; try { // 4. 执行目标方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // 5. 异常时回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 6. 清理事务信息 cleanupTransactionInfo(txInfo); } // 7. 正常返回时提交 commitTransactionAfterReturning(txInfo); return retVal; }其中步骤 4 到步骤 7 的顺序至关重要:先执行业务方法,再在方法正常返回后提交事务;如果业务方法抛出异常,则进入异常处理逻辑判断是否需要回滚。
五、创建事务并绑定到当前线程
当invokeWithinTransaction决定需要开启事务后,会调用createTransactionIfNecessary。该方法进一步委托给AbstractPlatformTransactionManager.getTransaction,这是 Spring 事务管理的另一个核心方法。
5.1 事务传播行为的判断
在getTransaction中,Spring 会根据传播行为决定是新建事务,还是加入已有事务。这里以最常见的REQUIRED为例来说明。
Spring 会先检查当前线程是否已经存在事务。它借助TransactionSynchronizationManager.getResource从线程绑定的资源中查找数据源对应的ConnectionHolder。如果已经存在连接,说明外层方法已经开启了事务,当前方法会直接加入该事务;如果不存在,则创建一个新事务。
可以将这段逻辑简化为以下示意代码:
public final TransactionStatus getTransaction(TransactionDefinition definition) { Object transaction = doGetTransaction(); if (isExistingTransaction(transaction)) { // 已经存在事务,按传播行为处理 return handleExistingTransaction(definition, transaction, false); } // 不存在事务,按传播行为决定是否新建 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, false, true, null); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } // 其它传播行为按 SUPPORTS、NOT_SUPPORTED 等处理 return null; }5.2 从数据源获取连接并关闭自动提交
新建事务时,DataSourceTransactionManager.doBegin会从DataSource中获取一个数据库连接,并把这个连接绑定到当前线程。与此同时,它会把连接的自动提交模式设置为false。
这一步是事务能够控制提交和回滚的根本原因。JDBC 连接默认是自动提交的,即每条 SQL 执行完都会立即提交。Spring 将autoCommit关闭后,后续所有通过该连接执行的 SQL 都只会在内存和数据库临时区域中生效,必须等待显式的commit或rollback调用才会真正落到数据库。
doBegin的关键步骤如下:
Connection newCon = obtainConnection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(newCon, definition); if (definition.isReadOnly() && definition.isIsolationLevelSet()) { // 只读且设置了隔离级别时执行设置 } if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); con.setAutoCommit(false); }可以看到,获取连接、关闭自动提交、绑定线程,这三步共同完成了事务环境的最初搭建。
六、正常提交:@Transactional 方法返回后发生了什么
当目标方法正常执行完毕,invokeWithinTransaction会调用commitTransactionAfterReturning,随后进入AbstractPlatformTransactionManager.commit。提交过程本身并非直接把数据库连接提交了事,而是包含一系列状态检查和处理。
6.1 commit 的主流程
commit方法首先检查事务状态。如果业务方法内部已经手动调用了setRollbackOnly或事务状态被标记为必须回滚,则即使方法正常返回,Spring 也会将其转为回滚。这一点常被用来解释:为什么代码没有抛异常,但事务仍然被回滚了。
正常情况下,commit会走到processCommit,其主要流程如下:
private void processCommit(DefaultTransactionStatus status) { try { boolean beforeCompletionInvoked = false; try { // 1. 事务提交前钩子 triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; // 2. 执行真正的数据库提交 doCommit(status); // 3. 提交后钩子 triggerAfterCommit(status); } finally { // 4. 完成清理 triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { // 5. 恢复资源状态 cleanupAfterCompletion(status); } }6.2 真正的数据库提交动作
真正执行数据库提交的是DataSourceTransactionManager.doCommit。它的实现非常直接,就是拿到当前事务绑定的 JDBC 连接,并调用连接的commit方法。
protected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); try { con.commit(); } catch (SQLException ex) { throw translateException("JDBC commit", ex); } }此时,业务方法期间执行的所有 SQL 才会被一次性提交到数据库。这也解释了为什么在事务方法执行过程中,数据库查询工具往往看不到中间结果,除非事务隔离级别和数据库实现允许读取未提交数据。
实战:一个完整的声明式事务示例
下面通过一个基于 Spring Boot 的转账场景,完整验证@Transactional在正常提交和异常回滚时的行为。示例使用JdbcTemplate操作数据库,并将数据库连接交由 Spring 事务管理器统一管理。
测试前先准备一张账户表,插入alice和bob两个用户,初始余额均为500。
示例表结构
CREATE TABLE account ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(64) NOT NULL, balance DECIMAL(10, 2) NOT NULL ); INSERT INTO account (user_name, balance) VALUES ('alice', 500.00); INSERT INTO account (user_name, balance) VALUES ('bob', 500.00);Service 类实现
AccountService提供两个转账方法:transferSuccess用于演示正常提交,transferWithRollback在完成转账更新后主动抛出运行时异常,用于演示事务回滚。
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; @Service public class AccountService { private final JdbcTemplate jdbcTemplate; public AccountService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 正常提交:两次账户更新同步生效 @Transactional public void transferSuccess(String from, String to, BigDecimal amount) { jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE user_name = ?", amount, from); jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE user_name = ?", amount, to); } // 异常回滚:方法内的两次更新最终都会被撤销 @Transactional public void transferWithRollback(String from, String to, BigDecimal amount) { jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE user_name = ?", amount, from); jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE user_name = ?", amount, to); if (amount.compareTo(new BigDecimal("1000")) >= 0) { throw new RuntimeException("转账金额过大,事务必须回滚"); } } }测试代码
测试类使用@SpringBootTest加载完整应用上下文,分别验证提交与回滚后的余额状态。
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest class AccountServiceTest { @Autowired private AccountService accountService; @Autowired private JdbcTemplate jdbcTemplate; @Test void transferSuccess_shouldCommit() { accountService.transferSuccess("alice", "bob", new BigDecimal("100")); BigDecimal aliceBalance = jdbcTemplate.queryForObject( "SELECT balance FROM account WHERE user_name = 'alice'", BigDecimal.class); BigDecimal bobBalance = jdbcTemplate.queryForObject( "SELECT balance FROM account WHERE user_name = 'bob'", BigDecimal.class); // 运行结果:提交成功,alice=400.00,bob=600.00 assertEquals(new BigDecimal("400.00"), aliceBalance); assertEquals(new BigDecimal("600.00"), bobBalance); } @Test void transferWithRollback_shouldRollback() { assertThrows(RuntimeException.class, () -> accountService.transferWithRollback("alice", "bob", new BigDecimal("1000"))); BigDecimal aliceBalance = jdbcTemplate.queryForObject( "SELECT balance FROM account WHERE user_name = 'alice'", BigDecimal.class); BigDecimal bobBalance = jdbcTemplate.queryForObject( "SELECT balance FROM account WHERE user_name = 'bob'", BigDecimal.class); // 运行结果:触发回滚,alice=500.00,bob=500.00 assertEquals(new BigDecimal("500.00"), aliceBalance); assertEquals(new BigDecimal("500.00"), bobBalance); } }运行结果说明
- transferSuccess 正常提交:方法正常返回后,
alice余额从 500 更新为 400,bob余额从 500 更新为 600,两条更新在同一个事务中同时提交。 - transferWithRollback 异常回滚:方法抛出
RuntimeException后,alice和bob的余额更新都被回滚,最终查询结果仍然保持为 500 和 500,证明异常发生前的数据库修改没有真正落库。
七、异常回滚:什么样的异常才算回滚异常
如果目标方法抛出异常,invokeWithinTransaction会进入completeTransactionAfterThrowing。这里最先执行的是txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus(), txInfo.getTransactionAttribute()),但真正决定是否回滚的,是TransactionAttribute.rollbackOn方法。
7.1 默认回滚规则
Spring 的默认规则是:
RuntimeException及其子类会触发回滚。Error会触发回滚。- 受检异常
Exception默认不会触发回滚,除非在@Transactional(rollbackFor = Exception.class)中显式指定。
这个规则来自RuleBasedTransactionAttribute。用户在注解中配置的rollbackFor和noRollbackFor会被转换成回滚规则列表,参与最终判断。
7.2 回滚流程与数据库回滚动作
一旦确认需要回滚,AbstractPlatformTransactionManager.rollback会调用processRollback,最后委托给DataSourceTransactionManager.doRollback,其实现与提交动作对称:
protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); try { con.rollback(); } catch (SQLException ex) { throw translateException("JDBC rollback", ex); } }连接执行rollback后,事务期间的所有修改都会被撤销,数据库重新回到事务开始前的状态。
7.3 回滚也需要恢复现场
无论提交还是回滚,最终都会进入cleanupAfterCompletion。这一步负责释放连接、恢复自动提交模式、解除线程绑定,并触发afterCompletion同步回调。如果使用了TransactionSynchronizationManager注册了事务同步器,就能在这一阶段收到事务完成的通知。
八、完整链路回顾
把上述流程串起来,一次典型的事务方法调用会经历以下完整阶段:
flowchart TD A[调用目标方法] --> B[TransactionInterceptor.invoke] B --> C[解析 TransactionAttribute] C --> D[getTransaction 处理传播行为] D --> E[从 DataSource 获取连接] E --> F[设置 autoCommit=false 并绑定线程] F --> G[执行目标业务方法] G --> H{业务方法是否抛异常} H -->|否| I[commitTransactionAfterReturning] H -->|是| J[completeTransactionAfterThrowing] I --> K[rollbackOn 判断] J --> K K -->|提交| L[Connection.commit] K -->|回滚| M[Connection.rollback] L --> N[cleanupAfterCompletion 恢复现场] M --> N从图中可以看出,@Transactional并没有改变 JDBC 的底层事务模型。它做的事情,是把原本需要开发者手动管理的连接获取、自动提交关闭、提交、回滚、资源释放这些步骤,通过代理和拦截器自动编排到方法调用的前后,从而让业务代码只关注业务本身。
九、为什么同类方法内部调用会失效
理解了代理机制后,就能解释一个高频问题:为什么在同一个类中,方法 A 直接调用带@Transactional的方法 B,B 的事务往往不会生效。
原因在于,Spring 事务是基于代理实现的。外部调用service.methodA()时,首先进入的是代理对象。代理对象在methodA执行前后完成事务拦截。但如果methodA内部通过this.methodB()调用 B,这里的this是目标对象本身,而不是代理对象,因此这次调用完全绕过了TransactionInterceptor。方法 B 上的事务注解自然无法生效。
常见的解决方式有三种:
- 将该事务方法拆分到另一个 Spring Bean 中,通过注入的代理对象调用。
- 通过
AopContext.currentProxy()获取当前代理对象,再调用目标方法。这种方式需要开启exposeProxy配置。 - 注入自身代理,前提是处理好循环依赖或合理地分离职责。
大部分项目中最推荐的,是第一种方式,即把不同事务边界的方法拆到不同的 Service 中,既保证代理生效,也让事务边界更加清晰。
十、核心类与源码入口速查
为了便于后续自行阅读源码,这里整理一份核心类及其职责的对照表。
| 类名 | 主要职责 |
|---|---|
| ProxyTransactionManagementConfiguration | 注册事务基础设施,创建 advisor、interceptor 等 Bean |
| AnnotationTransactionAttributeSource | 从注解中解析事务属性 |
| BeanFactoryTransactionAttributeSourceAdvisor | 判断哪些 Bean 需要创建事务代理 |
| TransactionInterceptor | 事务调用的拦截入口 |
| TransactionAspectSupport | 承载核心流程与线程上下文管理 |
| AbstractPlatformTransactionManager | 定义提交、回滚、资源清理的模板流程 |
| DataSourceTransactionManager | 基于 JDBC 连接实现真正的事务操作 |
| TransactionSynchronizationManager | 管理事务资源与同步器的线程绑定 |
十一、总结
Spring 声明式事务的本质,是利用 AOP 代理把事务管理逻辑织入到业务方法调用链中。@Transactional注解负责声明事务属性,TransactionAttributeSource负责解析这些属性,TransactionInterceptor负责拦截调用,而AbstractPlatformTransactionManager及其子类则负责真正的事务开启、提交和回滚。
整个流程的关键点可以归纳为:创建代理是前提,解析事务属性是入口,获取连接并关闭自动提交是核心,业务方法执行完毕后的提交和异常后的回滚是终点,而线程级的资源绑定则保证了同一事务链路中多次数据库操作能够共享同一个连接。理解这些机制,有助于在遇到事务不生效、回滚不彻底、连接泄漏等问题时,快速定位到具体环节并做出可靠修复。