ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

事务 Transaction 源码分析:@Transactional 如何控制数据库事务提交与回滚

事务 Transaction 源码分析:@Transactional 如何控制数据库事务提交与回滚 如果这篇文章对你有帮助欢迎关注我的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); // 运行结果提交成功alice400.00bob600.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); // 运行结果触发回滚alice500.00bob500.00 assertEquals(new BigDecimal(500.00), aliceBalance); assertEquals(new BigDecimal(500.00), bobBalance); } }运行结果说明transferSuccess 正常提交方法正常返回后alice余额从 500 更新为 400bob余额从 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[设置 autoCommitfalse 并绑定线程] 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的方法 BB 的事务往往不会生效。原因在于Spring 事务是基于代理实现的。外部调用service.methodA()时首先进入的是代理对象。代理对象在methodA执行前后完成事务拦截。但如果methodA内部通过this.methodB()调用 B这里的this是目标对象本身而不是代理对象因此这次调用完全绕过了TransactionInterceptor。方法 B 上的事务注解自然无法生效。常见的解决方式有三种将该事务方法拆分到另一个 Spring Bean 中通过注入的代理对象调用。通过AopContext.currentProxy()获取当前代理对象再调用目标方法。这种方式需要开启exposeProxy配置。注入自身代理前提是处理好循环依赖或合理地分离职责。大部分项目中最推荐的是第一种方式即把不同事务边界的方法拆到不同的 Service 中既保证代理生效也让事务边界更加清晰。十、核心类与源码入口速查为了便于后续自行阅读源码这里整理一份核心类及其职责的对照表。类名主要职责ProxyTransactionManagementConfiguration注册事务基础设施创建 advisor、interceptor 等 BeanAnnotationTransactionAttributeSource从注解中解析事务属性BeanFactoryTransactionAttributeSourceAdvisor判断哪些 Bean 需要创建事务代理TransactionInterceptor事务调用的拦截入口TransactionAspectSupport承载核心流程与线程上下文管理AbstractPlatformTransactionManager定义提交、回滚、资源清理的模板流程DataSourceTransactionManager基于 JDBC 连接实现真正的事务操作TransactionSynchronizationManager管理事务资源与同步器的线程绑定十一、总结Spring 声明式事务的本质是利用 AOP 代理把事务管理逻辑织入到业务方法调用链中。Transactional注解负责声明事务属性TransactionAttributeSource负责解析这些属性TransactionInterceptor负责拦截调用而AbstractPlatformTransactionManager及其子类则负责真正的事务开启、提交和回滚。整个流程的关键点可以归纳为创建代理是前提解析事务属性是入口获取连接并关闭自动提交是核心业务方法执行完毕后的提交和异常后的回滚是终点而线程级的资源绑定则保证了同一事务链路中多次数据库操作能够共享同一个连接。理解这些机制有助于在遇到事务不生效、回滚不彻底、连接泄漏等问题时快速定位到具体环节并做出可靠修复。
返回列表