📌 PDF:大白话说Java面试题 — 06_Spring篇
第19题:Spring 事务的底层原理
📚 回答:
- 核心考点: Spring 事务 的底层原理远不止"AOP + 动态代理"这么简单。大厂面试中,面试官期望你深入理解 声明式事务 vs 编程式事务的选型差异、@EnableTransactionManagement 的启动链路(AutoProxyRegistrar + ProxyTransactionManagementConfiguration)、TransactionInterceptor 的完整拦截流程(createTransactionIfNecessary → proceedWithInvocation → commit/rollback)、七种传播行为的底层实现(ThreadLocal 绑定 ConnectionHolder)、回滚规则的精确语义(默认仅回滚 RuntimeException/Error,checked 异常需显式配置),以及 事务失效的 10 大经典场景。面试官真正想判断的是:你是否能从源码级理解 Spring 如何将事务逻辑编织到业务方法中,以及能否在生产环境中快速定位和解决事务问题。
1. Spring 事务的两种实现方式
-
1.1 编程式事务 通过
TransactionTemplate或PlatformTransactionManager手动控制事务边界,灵活性高但代码侵入性强。// 编程式事务示例 @Autowired private TransactionTemplate transactionTemplate; public void executeWithTx() { transactionTemplate.execute(status -> { userDao.insert(user); orderDao.insert(order); return null; }); }缺点:业务代码中掺杂事务管理逻辑,可读性差,维护成本高。
-
1.2 声明式事务 通过
@Transactional注解 + AOP 实现,零侵入,最细粒度到方法级别。@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void executeWithTx() { userDao.insert(user); orderDao.insert(order); }优点:业务代码纯净,事务逻辑与业务逻辑解耦。 缺点:最细粒度只能到方法级别,无法像编程式事务那样精确控制代码块级别的事务边界。[citation:0]
选型建议:绝大多数场景使用声明式事务;仅在需要精细控制事务边界(如部分提交、嵌套事务复杂交互)时使用编程式事务。
2. 声明式事务的启动链路------@EnableTransactionManagement 做了什么?
-
2.1 核心注解解析
@EnableTransactionManagement通过@Import(TransactionManagementConfigurationSelector.class)向 Spring 容器注册两个关键组件:[citation:1]组件 作用 关键代码 AutoProxyRegistrar 启用 AOP 自动代理功能,注册 InfrastructureAdvisorAutoProxyCreator(BeanPostProcessor),拦截所有 Bean 的创建过程AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)ProxyTransactionManagementConfiguration 注册事务 Advisor、TransactionAttributeSource、TransactionInterceptor BeanFactoryTransactionAttributeSourceAdvisor+TransactionInterceptor -
2.2 代理创建规则
InfrastructureAdvisorAutoProxyCreator在 Bean 创建后判断是否需要创建代理:- 从当前类向上遍历(父类、接口、父接口),检查是否有
@Transactional注解; - 检查类的任意 public 方法上是否有
@Transactional注解; - 若满足条件,则通过
DefaultAopProxyFactory创建代理(实现接口用 JDK 动态代理,否则用 CGLIB)。[citation:0]
// DefaultAopProxyFactory 源码 public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); // JDK 动态代理 } return new ObjenesisCglibAopProxy(config); // CGLIB 代理 } return new JdkDynamicAopProxy(config); } - 从当前类向上遍历(父类、接口、父接口),检查是否有
3. TransactionInterceptor 的完整拦截流程
-
3.1 核心入口:invokeWithinTransaction 当代理对象的方法被调用时,实际执行的是
TransactionInterceptor.invoke(),其内部调用invokeWithinTransaction()完成事务全生命周期管理:[citation:1]protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // 1. 解析 @Transactional 注解,获取事务属性 final TransactionAttribute txAttr = getTransactionAttributeSource() .getTransactionAttribute(method, targetClass); // 2. 确定事务管理器 final TransactionManager tm = determineTransactionManager(txAttr); PlatformTransactionManager ptm = asPlatformTransactionManager(tm); if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { // 3. 创建事务(或加入已有事务) TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // 4. 执行业务方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // 5. 异常处理:根据回滚规则决定提交或回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 6. 清理事务信息(从 ThreadLocal 中移除) cleanupTransactionInfo(txInfo); } // 7. 正常完成,提交事务 commitTransactionAfterReturning(txInfo); return retVal; } } -
3.2 事务开启:createTransactionIfNecessary 内部调用
getTransaction(),根据传播行为决定是创建新事务还是加入已有事务:- 从
DataSourceUtils.getConnection(dataSource)获取数据库连接; - 设置
autoCommit = false(这是 Spring 事务的核心,禁止自动提交);[citation:2] - 将连接绑定到当前线程的
ThreadLocal<ConnectionHolder>中,保证同一事务内多个 DAO 操作共享同一连接。
- 从
-
3.3 事务提交与回滚
场景 行为 源码方法 方法正常返回 提交事务 commitTransactionAfterReturning()抛出 RuntimeException / Error 回滚事务 completeTransactionAfterThrowing()抛出 checked Exception(默认) 提交事务 completeTransactionAfterThrowing()异常被 try-catch 未抛出 提交事务 异常未传播到拦截器 关键陷阱:默认情况下只有 RuntimeException 和 Error 会触发回滚,checked Exception(如
IOException、SQLException)不会回滚!必须通过rollbackFor = Exception.class显式配置。[citation:3]
4. 七种事务传播行为深度解析
Spring 事务传播机制通过 ThreadLocal 实现,本质上是维护当前线程的事务上下文(TransactionSynchronizationManager 中的 resources ThreadLocalMap)。[citation:4]
| 传播行为 | 含义 | 底层实现 | 适用场景 |
|---|---|---|---|
| REQUIRED(默认) | 当前有事务则加入,无则新建 | 检查 ThreadLocal 中是否存在连接,存在则复用 | 绝大多数业务方法 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 将当前连接解绑到 suspendedResources,创建新连接绑定 | 日志记录、审计等独立操作 |
| NESTED | 在当前事务中创建保存点(Savepoint) | 利用 JDBC Savepoint 机制 | 部分回滚场景 |
| SUPPORTS | 有事务则加入,无则以非事务执行 | 检查 ThreadLocal,无则直接执行 | 查询方法 |
| NOT_SUPPORTED | 挂起当前事务,以非事务执行 | 解绑当前连接,执行完再恢复 | 不需要事务的批量操作 |
| MANDATORY | 必须有事务,否则抛异常 | 检查 ThreadLocal,无则抛 IllegalTransactionStateException | 强制事务上下文的方法 |
| NEVER | 必须无事务,否则抛异常 | 检查 ThreadLocal,有则抛异常 | 纯查询方法禁止事务 |
-
REQUIRED vs REQUIRES_NEW 的核心差异:
// 场景:methodA (REQUIRED) 调用 methodB (REQUIRES_NEW) @Transactional public void methodA() { dao.insertA(); // 在事务 T1 中 serviceB.methodB(); // 挂起 T1,开启 T2 throw new RuntimeException(); // T1 回滚,但 T2 已独立提交! } @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() { dao.insertB(); // 在独立事务 T2 中 }methodB的事务 T2 与methodA的事务 T1 完全独立,T1 回滚不影响 T2。这是日志记录、审计等"必须成功"操作的标准用法。[citation:5]
5. 事务失效的 10 大经典场景
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 1. 方法非 public | @Transactional 只能作用于 public 方法,非 public 方法代理不拦截 | 改为 public,或改用编程式事务 |
| 2. 同类方法自调用 | this.method() 不走代理,直接调用目标对象 | 注入自身代理,或改用 AopContext.currentProxy() [citation:0] |
| 3. 异常被 try-catch 吞掉 | 异常未传播到 TransactionInterceptor,拦截器感知不到异常 | catch 后重新抛出,或手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() |
| 4. 抛出 checked Exception | 默认不回滚 checked 异常 | 配置 rollbackFor = Exception.class [citation:3] |
| 5. 错误配置 noRollbackFor | 显式排除了本应回滚的异常 | 检查 noRollbackFor 配置 |
| 6. 数据库引擎不支持事务 | MyISAM 引擎不支持事务 | 改用 InnoDB |
| 7. 传播行为配置错误 | 如 NOT_SUPPORTED 导致方法非事务执行 | 根据业务语义选择正确的传播行为 |
| 8. 异步方法(@Async) | @Async 会在新线程执行,ThreadLocal 事务上下文丢失 | 异步方法内部重新开启事务,或改用分布式事务 |
| 9. 多数据源未指定事务管理器 | @Transactional 未指定 transactionManager,使用默认的可能不匹配 | 显式指定 transactionManager = "xxxManager" |
| 10. 事务方法被 final/static 修饰 | CGLIB 代理通过继承实现,final/static 方法无法被代理覆盖 | 去掉 final/static 修饰 |
-
同类自调用陷阱详解:
@Service public class OrderService { // ❌ 错误:methodA 调用 methodB 不走代理,事务失效! public void methodA() { this.methodB(); // 直接调用,不经过 TransactionInterceptor } @Transactional public void methodB() { orderDao.insert(order); } } // ✅ 正确:注入自身代理对象 @Service public class OrderService { @Autowired private OrderService self; // 注入代理对象 public void methodA() { self.methodB(); // 通过代理调用,事务生效 } @Transactional public void methodB() { orderDao.insert(order); } }
6. 核心组件深度对比
| 组件 | 接口/类 | 职责 | 典型实现 |
|---|---|---|---|
| 事务管理器 | PlatformTransactionManager | 定义事务的开启、提交、回滚标准接口 | DataSourceTransactionManager(JDBC)、JpaTransactionManager(JPA) |
| 事务定义 | TransactionDefinition | 封装隔离级别、传播行为、超时、只读等属性 | DefaultTransactionDefinition |
| 事务状态 | TransactionStatus | 记录事务当前状态(是否新事务、是否回滚标记) | DefaultTransactionStatus |
| 事务属性源 | TransactionAttributeSource | 解析 @Transactional 注解获取事务属性 | AnnotationTransactionAttributeSource |
| 事务拦截器 | TransactionInterceptor | AOP 拦截器,编织事务逻辑到目标方法 | TransactionInterceptor |
| 事务同步管理器 | TransactionSynchronizationManager | 通过 ThreadLocal 管理当前线程的事务资源 | TransactionSynchronizationManager |
7. 面试官追问与高分回答模板
-
追问 1:“Spring 事务的底层原理是什么?”
低分回答:“基于 AOP 和动态代理,通过 TransactionInterceptor 拦截方法。”(太浅,没有触及启动链路和传播机制)
高分回答: > “Spring 声明式事务的底层是一个完整的 AOP 链路: > 1. 启动阶段:
@EnableTransactionManagement通过@Import注册AutoProxyRegistrar(启用 AOP 自动代理)和ProxyTransactionManagementConfiguration(注册TransactionInterceptor)。 > 2. 代理阶段:InfrastructureAdvisorAutoProxyCreator判断 Bean 是否含@Transactional,有则创建 JDK 或 CGLIB 代理。 > 3. 运行时阶段:方法调用被TransactionInterceptor.invoke()拦截,内部调用invokeWithinTransaction():先通过TransactionAttributeSource解析注解属性,再调用PlatformTransactionManager.getTransaction()根据传播行为获取/创建事务(核心是将数据库连接绑定到 ThreadLocal),然后执行业务方法,最后根据异常类型决定提交或回滚。 > 4. 资源清理:cleanupTransactionInfo()从 ThreadLocal 中移除事务信息,防止内存泄漏。” -
追问 2:“@Transactional 默认在什么异常下回滚?checked 异常为什么不回滚?”
低分回答:“默认回滚 RuntimeException。”(没有解释设计原因)
高分回答: > “默认只回滚
RuntimeException和Error,不回滚 checked Exception。这是 Spring 的设计选择: > -RuntimeException代表编程错误或不可恢复的系统异常,通常需要回滚; > - checked Exception 代表业务异常(如FileNotFoundException),业务层可能已做补偿处理,强制回滚可能破坏业务逻辑。 > 如果业务需要 checked 异常也回滚,必须显式配置rollbackFor = Exception.class。同时注意:如果异常在方法内被 try-catch 吞掉未抛出,拦截器感知不到异常,事务会正常提交。” -
追问 3:“REQUIRED 和 REQUIRES_NEW 有什么区别?什么场景用 REQUIRES_NEW?”
高分回答: > “核心区别在于 事务的独立性: > -
REQUIRED:当前有事务则加入,无则新建。子方法与父方法共享同一数据库连接,同生共死。 > -REQUIRES_NEW:挂起当前事务,新建完全独立的事务。子方法有自己的连接和提交/回滚逻辑,父事务回滚不影响子事务。 > 底层实现上,REQUIRES_NEW会将当前线程 ThreadLocal 中的连接解绑到suspendedResources,创建新连接绑定到 ThreadLocal,子方法执行完后再恢复父事务的连接。 > 典型场景:日志记录、审计操作。例如订单下单失败回滚时,操作日志必须保留,此时日志方法配置REQUIRES_NEW确保独立提交。” -
追问 4:“同类方法自调用为什么会导致事务失效?怎么解决?”
高分回答: > “根本原因是 Spring AOP 基于代理实现。当
methodA()内部调用this.methodB()时,this指向的是目标对象本身,而非代理对象。事务逻辑(开启、提交、回滚)都编织在代理对象中,直接调用目标对象绕过了TransactionInterceptor,导致事务失效。 > 解决方案有三种: > 1. 注入自身代理:@Autowired private OrderService self;然后self.methodB(); > 2. 使用AopContext.currentProxy():((OrderService) AopContext.currentProxy()).methodB(),需配置@EnableAspectJAutoProxy(exposeProxy = true); > 3. 拆分到另一个 Bean:将methodB移到OrderService2中,通过注入调用。” -
追问 5:“Spring 事务和数据库事务是什么关系?”
高分回答: > “Spring 事务是 对数据库事务的抽象和封装,本身不实现事务,最终依赖数据库的事务机制(如 MySQL 的 redo log/undo log、InnoDB 的 MVCC)。 > Spring 的核心贡献是: > 1. 统一抽象:通过
PlatformTransactionManager屏蔽 JDBC、JPA、Hibernate 等不同数据访问技术的事务差异; > 2. 声明式管理:通过 AOP 将事务逻辑从业务代码中剥离; > 3. 传播机制:通过 ThreadLocal 管理连接,实现跨方法的事务传播(这是 Spring 特有的,数据库本身没有"传播行为"概念)。 > 没有数据库的事务支持,Spring 无法提供事务功能。” -
追问 6:“如果事务方法里调用了异步方法(@Async),事务还能生效吗?”
高分回答: > “不能生效。
@Async会在新线程中执行方法,而 Spring 事务通过ThreadLocal绑定连接,新线程没有父线程的事务上下文,导致事务传播失效。 > 解决方案: > 1. 在异步方法内部也加上@Transactional,让它在自己的事务中执行; > 2. 如果要求异步方法参与父事务,必须使用分布式事务框架(如 Seata); > 3. 如果异步操作必须随父事务回滚,考虑改为同步执行。”
8. 方案选型速查表
| 业务场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 普通 CRUD 业务方法 | @Transactional(默认 REQUIRED) | 简单、无侵入、满足绝大多数场景 |
| 日志/审计(必须成功) | @Transactional(propagation = REQUIRES_NEW) | 独立于主事务,主事务回滚不影响 |
| 批量导入(部分失败容忍) | @Transactional(propagation = NESTED) | 利用 Savepoint 实现部分回滚 |
| 纯查询方法 | @Transactional(readOnly = true, propagation = SUPPORTS) | 只读优化,无事务时也不强制开启 |
| 不需要事务的批量操作 | propagation = NOT_SUPPORTED | 挂起事务,避免不必要的事务开销 |
| 强制要求事务上下文 | propagation = MANDATORY | 调用方必须已开启事务,否则抛异常 |
| 精细控制事务边界 | TransactionTemplate 编程式事务 | 代码块级别控制,灵活但侵入性强 |
| 多数据源事务 | JtaTransactionManager + XA | 分布式事务,两阶段提交 |
💡 面试官想要的满分总结:
Spring 声明式事务的本质是 AOP 代理 + ThreadLocal 资源绑定 的精密工程。核心链路是:
@EnableTransactionManagement启动 AOP 代理 →TransactionInterceptor拦截方法 →PlatformTransactionManager根据传播行为获取/创建事务 → 将连接绑定到 ThreadLocal → 执行业务方法 → 根据异常规则提交或回滚 → 清理 ThreadLocal。理解 Spring 事务必须抓住三个关键点:
- 代理是前提:只有经过代理对象调用的方法才会被
TransactionInterceptor拦截,同类自调用、非 public 方法都会导致事务失效;- 传播行为是核心:七种传播行为通过 ThreadLocal 中连接的挂起/恢复/复用来实现,
REQUIRES_NEW的独立性、NESTED的 Savepoint 机制是高频考点;- 回滚规则是陷阱:默认仅回滚
RuntimeException/Error,checked 异常和 try-catch 吞异常是生产环境最常见的两个坑。最后记住:Spring 事务是对数据库事务的抽象,不是替代。没有数据库的事务支持(如 MyISAM 引擎),Spring 事务完全无效。工程实践中,遇到事务问题首先检查代理是否生效(同类自调用?),其次检查异常是否传播到拦截器(被 catch 了?),最后检查传播行为和回滚规则配置是否正确。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

被折叠的 条评论
为什么被折叠?



