恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
面试官:说说 Spring 事务设计原理?面试必问!
首页
资讯中心
/
面试官:说说 Spring 事务设计原理?面试必问!
面试官:说说 Spring 事务设计原理?面试必问!
发布时间:2026/10/7 21:30:38
一、引言为什么面试官总盯着 Spring 事务不放在 Java 后端面试中Spring 事务几乎是必考项。很多候选人能张口就背「Spring 事务通过 AOP 实现」「Transactional 注解底层是动态代理」「事务的传播行为有七种」。但当面试官继续追问一句「那你说说 TransactionInterceptor 内部到底做了什么」「为什么自调用会让事务失效」「REQUIRES_NEW 和 NESTED 在底层有何本质区别」不少人就开始露怯。原因很简单大多数人只记住了结论却没有真正理解 Spring 事务的设计原理。本文尝试从面试官的视角把 Spring 事务从核心抽象、AOP 拦截、事务管理器体系、传播行为源码、同步机制到失效场景系统梳理一遍力求讲透「为什么这么设计」以及「源码里到底发生了什么」。阅读本文后你将能够清晰地回答以下高频问题Spring 事务的核心接口有哪些分别承担什么职责声明式事务的底层实现原理是什么TransactionInterceptor 的完整执行流程是怎样的七种传播行为在源码层面如何实现为什么事务方法自调用会失效事务失效的常见场景有哪些事务同步机制 TransactionSynchronizationManager 的作用是什么二、事务基础回顾先把地基打牢在深入 Spring 之前必须先回顾数据库事务的基本概念。如果这部分不扎实后面理解 Spring 的事务抽象会比较吃力。2.1 什么是事务事务是一组操作的集合这组操作要么全部成功要么全部失败。最经典的例子是银行转账A 账户扣款 100 元B 账户加款 100 元这两个操作必须同时成功或同时失败否则就会出现资金凭空消失或凭空多出的问题。事务存在的根本目的是保证数据的一致性。数据库通过事务把多个写操作绑定为一个原子单元并在系统故障、并发访问等复杂场景下维持数据的正确性。2.2 ACID 四大特性事务具备四个基本特性简称 ACID原子性Atomicity事务中的所有操作要么全部执行成功要么全部不执行。即使中途发生错误也必须回滚到事务开始前的状态。一致性Consistency事务执行前后数据库必须从一个一致性状态转换到另一个一致性状态。例如转账前后两个账户的总金额必须保持不变。隔离性Isolation多个事务并发执行时彼此之间不能互相干扰。一个事务的执行过程对其他事务应该是隔离的。持久性Durability事务一旦提交对数据的修改就是永久性的即使系统崩溃也不会丢失。其中隔离性是最复杂、也是面试最爱考的点因为它直接关系到并发场景下的数据正确性。2.3 数据库隔离级别如果事务完全串行执行隔离性自然最好但并发性能会非常差。数据库为了在隔离性和性能之间做权衡定义了四种隔离级别隔离级别脏读不可重复读幻读读未提交READ UNCOMMITTED可能可能可能读已提交READ COMMITTED不可能可能可能可重复读REPEATABLE READ不可能不可能可能MySQL 通过间隙锁基本解决串行化SERIALIZABLE不可能不可能不可能这里解释三个并发问题脏读一个事务读到了另一个事务尚未提交的数据。如果另一个事务最终回滚读到的就是无效的「脏数据」。不可重复读同一个事务内多次读取同一条数据结果不一致。原因是其他事务在两次读取之间提交了修改。幻读同一个事务内两次查询得到的记录条数不一致。原因是其他事务在两次查询之间插入或删除了记录。理解这三个概念非常重要因为 Spring 事务的隔离级别底层最终依赖数据库实现而 MySQL 的默认隔离级别是可重复读。2.4 事务传播行为传播行为解决的是这样一个问题当一个事务方法被另一个事务方法调用时应该如何处理事务边界是加入已有事务还是新建事务还是以非事务方式运行标准定义中有七种传播行为Spring 全部支持REQUIRED如果当前存在事务则加入否则新建事务。默认行为。REQUIRES_NEW每次都新建事务。如果当前存在事务则把当前事务挂起。SUPPORTS如果当前存在事务则加入否则以非事务方式运行。NOT_SUPPORTED以非事务方式运行。如果当前存在事务则把当前事务挂起。MANDATORY必须在事务中运行否则抛出异常。NEVER必须在非事务中运行否则抛出异常。NESTED如果当前存在事务则创建嵌套事务否则新建事务。其中REQUIRED、REQUIRES_NEW、NESTED 是面试重点后文会结合源码深入分析。三、Spring 事务的核心抽象三大接口Spring 并没有重新发明事务而是对底层 JDBC、JPA、Hibernate 等事务操作进行了统一抽象。这一设计思想正是 Spring 一贯的「抽象统一、屏蔽差异」策略。Spring 事务模块的核心抽象主要包含三个接口PlatformTransactionManager、TransactionDefinition和TransactionStatus。3.1 PlatformTransactionManager事务管理器PlatformTransactionManager 是事务管理的核心接口负责事务的创建、提交和回滚。其定义如下public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }这个接口只有三个方法设计非常简洁getTransaction根据事务定义获取事务状态。如果当前已有事务则根据传播行为决定是否加入如果没有事务则创建新事务。commit提交事务。rollback回滚事务。为什么叫 Platform因为它是跨平台的抽象。无论底层是 JDBC、JPA 还是 Hibernate上层处理事务的逻辑完全一致只需要针对不同数据访问技术提供不同的实现类即可。3.2 TransactionDefinition事务定义TransactionDefinition 描述了一个事务的基本属性包括传播行为、隔离级别、超时时间、是否只读以及事务名称。它相当于事务的「配置元数据」。public interface TransactionDefinition { int PROPAGATION_REQUIRED 0; int PROPAGATION_SUPPORTS 1; int PROPAGATION_MANDATORY 2; int PROPAGATION_REQUIRES_NEW 3; int PROPAGATION_NOT_SUPPORTED 4; int PROPAGATION_NEVER 5; int PROPAGATION_NESTED 6; int ISOLATION_DEFAULT -1; int ISOLATION_READ_UNCOMMITTED 1; int ISOLATION_READ_COMMITTED 2; int ISOLATION_REPEATABLE_READ 4; int ISOLATION_SERIALIZABLE 8; int TIMEOUT_DEFAULT -1; int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName(); }这里可以看到传播行为、隔离级别本质上都是 int 常量。之所以用 int 而不用枚举是因为 Spring 早期版本为了兼容性采用了常量设计。3.3 TransactionStatus事务状态TransactionStatus 表示事务的运行时状态它让事务管理器能够跟踪当前事务的执行情况并决定提交还是回滚。public interface TransactionStatus extends TransactionExecution, SavepointManager { boolean isNewTransaction(); boolean hasSavepoint(); void setRollbackOnly(); boolean isRollbackOnly(); void flush(); boolean isCompleted(); }其中几个关键方法isNewTransaction判断当前事务是否是新创建的。这个状态对传播行为至关重要。如果是加入已有事务该方法会返回 false如果是新建事务返回 true。setRollbackOnly / isRollbackOnly标记事务只能回滚。当内部方法捕获了异常但没有继续抛出时外部事务可以通过这个标记感知异常从而回滚。hasSavepoint是否存在保存点与 NESTED 传播行为相关。这套「定义 管理器 状态」的三元抽象把事务的配置、执行和状态完全解耦是 Spring 事务模块优雅设计的体现。四、Spring 事务的两种使用方式Spring 提供两种事务使用方式编程式事务和声明式事务。4.1 编程式事务编程式事务需要开发者在代码中手动控制事务的开启、提交和回滚。最直接的方式是使用 TransactionTemplateService public class AccountService { private final TransactionTemplate transactionTemplate; public AccountService(PlatformTransactionManager transactionManager) { this.transactionTemplate new TransactionTemplate(transactionManager); } public void transfer(Long fromId, Long toId, BigDecimal amount) { transactionTemplate.execute(status - { try { accountDao.decrease(fromId, amount); accountDao.increase(toId, amount); return null; } catch (Exception e) { status.setRollbackOnly(); throw e; } }); } }编程式事务的优点是控制粒度细、逻辑直观缺点是业务代码与事务代码耦合重复代码多。4.2 声明式事务声明式事务通过 AOP 将事务管理与业务代码完全解耦开发者只需要在方法上加上 Transactional 注解即可Service public class AccountService { Transactional(rollbackFor Exception.class) public void transfer(Long fromId, Long toId, BigDecimal amount) { accountDao.decrease(fromId, amount); accountDao.increase(toId, amount); } }声明式事务的优点是非侵入、代码简洁也是实际开发中最常见的方式。4.3 两种方式的底层一致性很多候选人以为编程式事务和声明式事务是两套完全不同的机制。实际上它们最终都汇聚到同一个核心PlatformTransactionManager。无论是 TransactionTemplate 的 execute 方法还是 Transactional 注解触发的拦截器最终都是调用事务管理器的 getTransaction、commit、rollback 方法。换句话说声明式事务只是编程式事务的「AOP 封装」。理解这一点是吃透 Spring 事务原理的关键。五、声明式事务的底层原理AOP 代理声明式事务之所以能够做到「加个注解就生效」核心依赖 Spring AOP。这一节我们从代理机制讲起。5.1 动态代理机制Spring AOP 底层有两种动态代理实现JDK 动态代理目标类必须实现接口通过 java.lang.reflect.Proxy 生成代理对象。CGLIB 代理目标类无需实现接口通过继承目标类并覆写方法生成子类代理。Spring 默认的策略是如果目标类实现了接口默认使用 JDK 动态代理。如果目标类没有实现接口使用 CGLIB 代理。可以通过 proxyTargetClass 属性强制使用 CGLIB。在 Spring Boot 2.x 中默认的代理策略会根据目标对象是否实现接口来决定。需要特别注意的是使用 CGLIB 代理时被代理的方法不能是 final 或 private否则无法被覆写。5.2 TransactionInterceptor事务拦截器当我们用 EnableTransactionManagement 开启事务管理后Spring 会向容器中注册一个 TransactionInterceptor。这个拦截器实现了 MethodInterceptor 接口是 AOP 中的「环绕通知」核心。public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable { public Object invoke(MethodInvocation invocation) throws Throwable { Class? targetClass AopUtils.getTargetClass(invocation.getThis()); return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); } }); } }当代理对象的目标方法被调用时调用链会经过 TransactionInterceptor.invoke 方法最终进入 invokeWithinTransaction 这个核心方法。这才是 Spring 事务真正的入口。5.3 事务属性解析在 invokeWithinTransaction 执行之前需要先获取目标方法的事务属性。事务属性来自 Transactional 注解的元数据Spring 通过 TransactionAttributeSource 进行解析。解析策略遵循「方法级优先于类级」的原则先查找方法上的 Transactional 注解如果方法上没有再查找方法所在类上的 Transactional 注解如果类上也没有再向父类或接口查找。解析结果为 TransactionAttribute 对象它继承自 TransactionDefinition包含了传播行为、隔离级别、超时、只读和回滚规则等完整信息。六、事务拦截器核心流程源码级剖析这一节是全文的重点。看懂 invokeWithinTransaction 的流程基本就能回答「Spring 事务底层是怎么工作的」这个问题。6.1 invokeWithinTransaction 整体流程TransactionAspectSupport.invokeWithinTransaction 是整个声明式事务的执行骨架。其简化后的逻辑如下protected Object invokeWithinTransaction(Method method, Class? targetClass, InvocationCallback invocation) throws Throwable { TransactionAttributeSource tas getTransactionAttributeSource(); TransactionAttribute txAttr (tas ! null ? tas.getTransactionAttribute(method, targetClass) : null); TransactionManager tm determineTransactionManager(txAttr); PlatformTransactionManager ptm asPlatformTransactionManager(tm); String joinpointIdentification methodIdentification(method, targetClass, txAttr); if (txAttr null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { TransactionInfo txInfo createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { retVal invocation.proceedWithInvocation(); } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; } // 省略回调式事务处理分支 }从这个骨架中可以提炼出五个关键步骤获取事务属性通过 TransactionAttributeSource 解析目标方法上的 Transactional 配置。确定事务管理器根据事务属性或默认配置找到对应的 PlatformTransactionManager。创建事务上下文调用 createTransactionIfNecessary内部会调用事务管理器的 getTransaction 方法在必要时开启事务。执行目标方法调用 invocation.proceedWithInvocation() 进入真正的业务逻辑。提交或回滚方法正常返回时调用 commitTransactionAfterReturning 提交事务抛出异常时调用 completeTransactionAfterThrowing 判断是否需要回滚最后在 finally 块中调用 cleanupTransactionInfo 清理事务上下文。6.2 createTransactionIfNecessary事务上下文的创建createTransactionIfNecessary 是 TransactionAspectSupport 中的一个关键方法。它不只负责「开启事务」更重要的是把事务相关的三样东西组装成一个可追踪的上下文并绑定到当前线程。其核心逻辑可以分为三步调用事务管理器的 getTransaction 方法拿到当前方法的 TransactionStatus。这一步内部会根据传播行为判断是新建事务、加入已有事务还是挂起当前事务。把 PlatformTransactionManager、TransactionAttribute、TransactionStatus 以及当前线程上旧的 TransactionInfo 封装成一个新的 TransactionInfo 对象。通过 bindToThread 方法把新 TransactionInfo 绑定到 ThreadLocal 中。这样后续任何一层被代理的方法都能通过 currentTransactionInfo() 拿到当前事务上下文异常处理和清理时也能准确恢复上一级事务。之所以要保存旧的 TransactionInfo是为了支持嵌套调用场景内层事务结束后需要把外层事务的信息恢复回线程否则外层方法在提交或回滚时会拿到错误的事务状态。6.3 七种传播行为在源码中的实现传播行为最终是在 AbstractPlatformTransactionManager.getTransaction 方法中完成的。这个方法是 Spring 事务传播逻辑的核心其简化流程如下public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { Object transaction doGetTransaction(); if (isExistingTransaction(transaction)) { return handleExistingTransaction(definition, transaction, false); } if (definition.getPropagationBehavior() TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( No existing transaction found for transaction marked with propagation mandatory); } if (definition.getPropagationBehavior() TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHolder suspendedResources suspend(null); try { DefaultTransactionStatus status newTransactionStatus( definition, transaction, true, true, false, null, null); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } boolean newSynchronization (getTransactionSynchronization() SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(definition, null, true, newSynchronization, false, null, null, null); }面试常问的三种传播行为可以这样理解REQUIRED如果当前没有事务走的是上面「新建事务」的分支调用 doBegin 真正开启物理事务如果当前已经有事务则直接返回已有事务isNewTransaction 为 false。REQUIRES_NEW在 handleExistingTransaction 中先把当前事务挂起也就是调用 suspend 把当前连接和同步信息保存到 SuspendedResourcesHolder然后重新走 doBegin 开启一个全新的物理事务。内层提交后再通过 resume 恢复外层事务。NESTED不会挂起外层事务而是利用 JDBC Savepoint 建立一个保存点。内层事务回滚时只回滚到保存点不会影响外层事务但外层事务如果最终回滚内层的修改也会一起被回滚。这就是它与 REQUIRES_NEW 最本质的区别。需要注意的是NESTED 依赖数据库对 Savepoint 的支持常见的 MySQL InnoDB 和 PostgreSQL 都支持但需要底层使用 JDBC 事务管理器而不是 JTA 这类不支持嵌套保存点的事务管理器。6.4 事务同步机制TransactionSynchronizationManager在整个事务执行过程中Spring 需要把「当前线程正在使用的事务资源」保存起来并允许开发者在事务的各个阶段注册回调。这件事由 TransactionSynchronizationManager 完成。它的底层是 ThreadLocal主要保存三类信息事务资源例如 DataSourceTransactionManager 会把当前线程持有的 ConnectionHolder 绑定到 ThreadLocal 上保证同一个事务内的多次数据库操作复用同一个连接。事务同步状态记录当前事务是否是新建的、是否有活跃的 synchronization 等。TransactionSynchronization 集合保存开发者注册的回调在事务提交、回滚、完成后依次触发。常见的应用场景是某些操作不适合和数据库事务绑定得太死但又希望在事务提交成功后再执行。例如发送 MQ 消息、刷新本地缓存等。此时可以注册 afterCommit 回调Transactional public void createOrder(Order order) { orderDao.insert(order); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { messageSender.send(orderCreatedEvent(order)); } }); }这样只有当事务真正提交后消息才会发送如果事务回滚消息不会发出避免了「数据库没提交但消息已经发出」的数据不一致问题。七、Spring 事务失效的常见场景事务失效是面试中的高频追问。很多开发者以为加一个 Transactional 就能一劳永逸但实际上有不少场景会导致注解形同虚设。下面按照出现频率逐条分析。7.1 自调用导致事务失效这是最经典的失效场景。声明式事务依赖 AOP 代理只有在外部通过代理对象调用方法时TransactionalInterceptor 才会介入。如果在一个类内部通过 this 调用另一个带 Transactional 的方法调用直接发生在原始对象上根本不会经过代理。Service public class UserService { Transactional public void outer() { this.inner(); // 自调用不会经过代理inner 的事务失效 } Transactional(propagation Propagation.REQUIRES_NEW) public void inner() { // 期望开启独立新事务实际并不会生效 } }解决办法通常有三种把被调用的方法拆分到另一个 Bean 中通过注入的 Bean 调用或者通过 AopContext.currentProxy() 获取当前代理对象后调用也可以在 Spring Boot 项目中使用 EnableAspectJAutoProxy(exposeProxy true) 暴露代理对象。7.2 异常被方法内部捕获事务是否回滚取决于异常是否继续向上抛出到 TransactionInterceptor。如果业务方法内部通过 try-catch 把异常吞掉拦截器感知不到任何异常自然认为方法正常执行并提交事务。Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { try { accountDao.decrease(fromId, amount); accountDao.increase(toId, amount); } catch (Exception e) { log.error(转账失败, e); // 异常被吞掉事务仍然会提交 } }如果确实需要捕获异常可以在 catch 块中主动设置 currentTransactionStatus().setRollbackOnly()或者重新抛出异常让拦截器执行回滚逻辑。7.3 方法不是 publicSpring 声明式事务的底层代理只能拦截到外部可访问的方法。对于 private、protected 或包级私有方法JDK 动态代理和 CGLIB 代理都无法对它们进行有效增强。特别是 private 方法CGLIB 也无法覆写事务必然失效。Service public class OrderService { Transactional private void updateInternal(Long id, Integer status) { // private 方法无法被代理增强事务不生效 orderDao.updateStatus(id, status); } }因此需要事务管理的方法必须声明为 public并且通过 Spring 容器注入的代理对象来调用。7.4 回滚异常类型不匹配默认情况下Spring 只对 RuntimeException 和 Error 进行回滚对受检异常不会回滚。如果业务方法声明抛出 IOException 等受检异常即使异常被拦截器捕获事务也只会提交。Transactional public void importData() throws IOException { fileService.parse(); // 抛出 IOException默认不会回滚 } Transactional(rollbackFor Exception.class) public void importDataWithRollback() throws IOException { fileService.parse(); // 明确指定 Exception 后才会回滚 }面试或规范审查中通常建议统一写成 Transactional(rollbackFor Exception.class)让所有业务异常都能触发回滚。7.5 目标类被 final 修饰或方法被 final、static 修饰当使用 CGLIB 代理时代理类是目标类的子类依赖继承和覆写来增强方法。如果目标类被 final 修饰子类无法继承如果方法被 final 修饰子类无法覆写如果方法是 static它属于类而不是实例也不会走代理。这些情况下事务都会失效。7.6 数据库或存储引擎不支持事务事务最终要落到数据库层。如果底层数据源是 MySQL 的 MyISAM 存储引擎或者某些不支持事务的 NoSQL 数据源即使 Spring 层面配置完全正确也无法提供真正的 ACID 保障。需要确认存储引擎为 InnoDB 等支持事务的引擎并且事务管理器与数据源匹配。八、总结用一条主线串起 Spring 事务如果把 Spring 事务的知识点压缩成一条回答主线可以这样梳理一层抽象Spring 通过 PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三个接口统一了不同数据访问技术的事务操作。一种增强声明式事务本质上是 AOP通过 JDK 动态代理或 CGLIB 生成代理对象由 TransactionInterceptor 在执行方法前后织入事务逻辑。一条流程invokeWithinTransaction 负责解析事务属性、确定事务管理器、创建事务上下文、执行目标方法并根据异常情况决定提交或回滚。一组行为传播行为由 AbstractPlatformTransactionManager.getTransaction 实现其中 REQUIRED、REQUIRES_NEW 和 NESTED 是理解事务嵌套关系的关键。一道防线理解自调用、异常被吞、非 public 方法、回滚类型不匹配等失效场景才能写出真正可靠的事务代码。掌握了这条主线无论是解释 Spring 事务的启动过程、传播行为还是分析线上事务不生效的问题都能给出清晰、有层次的回答。建议读者结合实际调试在 TransactionInterceptor.invoke 和 AbstractPlatformTransactionManager.getTransaction 两处打上断点观察代理调用链和事务状态的流转会对本文内容有更直观的理解。