彻底搞懂 Spring @Transactional 底层原理:AOP、代理机制与源码 Debug 实战

Carlos 发布于 2026-03-08 58 次阅读


前言:如果你在使用 Spring 框架,必定接触过 @Transactional 注解。但你是否好奇过:写个注解就能自动提交和回滚,Spring 究竟在背后施了什么魔法?本文将带你扒开 @Transactional 的外衣,从宏观设计到底层源码 Debug,一步步搞透它的工作机制!

1. 业务场景:为什么我们需要事务?

在探究原理前,我们先看一个经典的转账场景 :

假设有一个 transfer() 方法,需要将资金从“发送方”转给“接收方”。代码逻辑主要有两步:

  1. 从发送方账户扣除金额,更新数据库。
  2. 给接收方账户增加金额,更新数据库。

痛点:这是完美的“快乐路径(Happy Path)”。但如果在第一步执行完后,接收方系统宕机了怎么办?此时发送方的钱扣了,接收方却没拿到钱,数据出现了严重的不一致,钱凭空消失了!

解决:这正是事务管理大显身手的地方。它保证了这组数据库操作要么全部成功(Commit),要么一旦中途出错就全部撤销(Rollback),从而保证系统数据的一致性。

2. 核心基础:AOP 与环绕通知 (Around Advice)

当我们给方法加上 @Transactional 时,Spring 在底层其实使用了 AOP(面向切面编程)

AOP 允许我们在不修改核心业务代码的情况下,把一些“次要逻辑”(如日志、安全、事务)插入到方法执行的前后。

对于事务处理,Spring 具体使用的是 环绕通知(Around Advice),它的逻辑极其简单粗暴:

  • 方法执行前:创建或获取一个数据库事务。
  • 执行目标方法:执行你写的转账业务逻辑(transfer)。
  • 方法执行后
    • 如果一切顺利(Success):提交事务(Commit)。
    • 如果抛出异常(Failure):回滚事务(Rollback)。

3. 幕后黑手:动态代理类 (Dynamic Proxy)

你以为程序运行的是你写的那个 AccountService 吗?并不是!

当 Spring 发现你的类带有 @Transactional 注解时,它会偷偷为你创建一个代理类(Proxy Class)

  • 接口实现类:如果你的类实现了接口,Spring 会使用 JDK 动态代理
  • 普通具体类:如果你的类没有实现接口,Spring 会使用 CGLIB 创建一个子类作为代理(类名通常带有 $$EnhancerBySpringCGLIB...)。

这个代理类会重写(Override)你的 transfer 方法。但代理类并不打算自己处理复杂的事务创建和回滚逻辑,它选择把活儿包出去——交给了真正的“劳模”组件:TransactionInterceptor

4. 核心大脑:TransactionInterceptor 组件

代理类在执行转账方法前,会把你的方法元数据(类名、方法名、参数等)打包,统统交给 TransactionInterceptor

这是 Spring 设计的高明之处:开发者无需关心底层的连接获取、提交和回滚,你只需要用反射告诉我执行什么方法,由 TransactionInterceptor 来套用那套“环绕通知”的模板逻辑。

千万别这么说!Spring 框架最喜欢玩的就是“表面一套,背后一套”的魔法,底层封装得很深。初学者看不懂这张图太正常了,这绝不是你的问题。

这张图(图 2)其实就是扒开了 @Transactional(图 1)的外衣,给你看 Spring 背后到底干了什么。

为了让你秒懂,我们用**“老板交代秘书办事”**的例子来比喻,咱们一步步看这张图:

1. 狸猫换太子(图中 Step 1 -> 2)

  • 你以为的: 你写了一个 AccountService(包含 transfer 转账方法),你以为程序运行时,别人调用的是你亲自写的这个类。
  • 实际上的(Proxy Created): Spring 框架看到你加了 @Transactional 这个标签,它心想:“这事儿很重要,不能让他直接搞。” 于是,Spring 偷偷制造了一个替身(动态代理类,图中的 CGLIB)
  • 结果: 这个替身长得跟你的 AccountService 一模一样,但它里面是个**“秘书”**。当外部想要调用 transfer 转账时,其实是这个秘书接过了活儿。

2. 秘书的固定套路(图中 Step 2 -> 3)

替身秘书接到转账任务后,并没有立刻去转账,而是掏出了一本《事务管理操作指南》——这就是图中的 TransactionInterceptor(事务拦截器)

3. 最核心的魔法:包裹你的代码(图中最下方的框框)

这就是整张图最核心的地方!秘书(拦截器)是怎么干活的呢?你看图里那个 try...catch 代码块,它把你写的真实代码包在了中间

  1. 准备工作(txManager.getTransaction():秘书在真正转账前,先跑去数据库那里喊一嗓子:“我要开启一个事务了啊!接下来发生的事情,全给我记到账本上!”(对应图里的 Before method execution)。
  2. 执行你的代码(method.invoke():这时候,秘书才把你从床上叫起来,让你去执行真正的“扣钱、加钱”逻辑(对应图里的 Target Method)。
  3. 顺利完成(txManager.commit():如果你的代码顺顺利利跑完了,什么错都没报。秘书就跑去告诉数据库:“老板事情办得很漂亮,把刚才的操作正式提交(Commit),永久保存!”(对应 After method execution (success))。
  4. 出了大问题(catch ... txManager.rollback():如果在执行你的代码时,突然系统断电了,或者报错抛异常了。秘书立马接管局面,跑去告诉数据库:“刚才全乱套了,不作数,全部给我回滚(Rollback),恢复到转账前的状态!”(对应 After method execution (failure))。

5. 源码级 Debug 走读:invoke() 方法内幕

为了证明上述理论,视频带我们直接杀入了 Spring 源码,拦截在 TransactionInterceptor 的核心方法——invoke() 上 。

当请求打进来时,invoke() 方法内部有几个极其关键的步骤:

  1. 获取事务属性:解析你的 @Transactional 注解,看看你配置的隔离级别(Isolation)和传播机制(Propagation)是什么。
  2. 决定事务管理器 (Transaction Manager):Spring 会通过自动装配(Auto-Configuration)来推断当前该用哪个事务管理器 。比如如果你用的是 JPA,它就会为你悄悄注入 JpaTransactionManager
  3. 获取/开启事务:根据传播机制,决定是加入现有事务还是新开一个事务。
  4. 执行目标方法 (invocation.proceed()):这里才是真正去执行你写的转账逻辑。
  5. 异常与回滚:如果目标方法抛出异常,代码会进入 catch 块,执行 completeTransactionAfterThrowing() 进行数据回滚。
  6. 成功与提交:如果方法正常返回,代码执行 commitTransactionAfterReturning() 将数据永久落盘。

🔍 Debug 实战演示

视频最后通过 Postman/Swagger 模拟了两种真实场景的 Debug:

  • 快乐路径 (Happy Scenario) :代码正常执行完 transfer 里的所有数据库更新操作。没有报错,顺利跳过异常捕获区,最终走到 commitTransactionAfterReturning()。打开数据库一看,发送方扣款 100,接收方加款 100,完美!
  • 灾难路径 (Failure Scenario) :作者在代码中强行抛出了一个“接收方服务器宕机”的异常。当代码执行到此处时,直接炸到 TransactionInterceptorcatch 块。事务拦截器立即调用 rollback 相关逻辑。再次刷新数据库,发送方的钱原封不动,保证了资金安全!

秘书的 6 步工作流 (invoke() 方法大揭秘)

  • 第 1 步:看老板的便签(获取事务属性) 秘书接到转账任务,先看一眼你贴在代码上的 @Transactional 标签。秘书心想:“嗯,老板要求这是个最高级别的绝密任务(隔离级别),而且必须有保镖跟着(传播机制)。”
  • 第 2 步:找对口的财务主管(决定事务管理器) 公司里有很多财务(比如管 MySQL 的、管 Oracle 的)。秘书看了一眼公司的配置,发现你用的是 JPA 技术,于是立马喊来了 JpaTransactionManager(JPA 财务主管)来负责记账。
  • 第 3 步:翻开新的账本(获取/开启事务) 准备工作就绪,秘书对财务主管说:“铺开一张全新的白纸,准备开始记账!从现在起的操作,全部算一笔账!”
  • 第 4 步:老板亲自下场干活(执行目标方法 invocation.proceed() 这是最核心的一步!这里代表着真正的你出场了。秘书说:“老板,舞台搭好了,你可以开始转账了!” 于是系统开始执行你写的“扣 A 账户 100 块,加 B 账户 100 块”的代码。
  • 第 5 步 或 第 6 步:看老板干活的结果(异常与回滚 / 成功与提交) 干活只有两种结果,于是就有了所谓的**“快乐路径”“灾难路径”**:

🌟 场景一:快乐路径 (Happy Scenario) -> 一切顺利

  1. 你(老板)把代码写得毫无破绽,A 扣钱成功,B 加钱成功。
  2. 整个过程行云流水,没有报错
  3. 秘书在旁边一看,激动地喊出那个长长的绝招:commitTransactionAfterReturning()
  4. 大白话:秘书对财务主管说:“老板干得漂亮,赶紧把刚才写在草稿纸上的账,**正式抄写(Commit)**到公司大账本里,盖章生效!”(最终数据库被完美更新)。

💥 场景二:灾难路径 (Failure Scenario) -> 突然翻车

  1. 你(老板)刚把 A 的钱扣掉,正准备给 B 加钱时,突然停电了,或者 B 的银行卡被冻结了!
  2. 你的代码瞬间“炸毛”了,抛出了一个异常(Exception)
  3. 秘书反应极快,眼疾手快地用一个叫 catch 的安全网把你接住,并喊出另一个绝招:completeTransactionAfterThrowing()
  4. 大白话:秘书对财务主管说:“快快快,刚才老板转账搞砸了!把刚才那张写了一半的草稿纸直接撕掉(Rollback 回滚),当做什么都没发生过!”
  5. 结果就是:A 的钱一点没少,B 也没多,虽然事情没办成,但钱没丢,绝对安全。

🎯 总结

Spring @Transactional 的底层魔法并不是黑科技,其核心链路如下:

注解标记 $\rightarrow$ 动态代理生成 $\rightarrow$ 拦截器(TransactionInterceptor)接管 $\rightarrow$ AOP 环绕执行(开启事务 $\rightarrow$ 核心业务 $\rightarrow$ 成功则提交 / 异常则回滚)

建议大家在平时开发时,也可以像视频中一样,在 TransactionInterceptor.invoke() 处打个断点,亲自体验一次 Spring 底层的流转之美!


(本文笔记基于 YouTube 频道 @Javatechie 的精彩讲解视频整理,如果你想看详细的 Debug 实操画面,推荐搭配原视频共同食用!)

🏆 大厂面试标准答案:Spring @Transactional 底层原理

1. 核心定位(这是什么?) @Transactional 是 Spring 提供的声明式事务管理机制,通常标注在 Service 层的类或方法上。它的核心目的是为了保证跨多个数据库 DML(增删改)操作时的数据一致性,确保业务逻辑满足事务的原子性(Atomicity),即“要么全部提交,要么全部回滚”。

2. 底层机制(它是怎么生效的?) 它的底层完全依赖于 Spring AOP(面向切面编程)动态代理机制。 当 Spring 容器启动并初始化 Bean 时,如果扫描到带有 @Transactional 注解的类,Spring 不会直接把这个原始的 Bean 放到容器中,而是会利用 AOP 为它动态生成一个代理对象(Proxy Object)

  • 补充细节:如果目标类实现了接口,Spring 默认使用 JDK 动态代理;如果是一个没有实现接口的普通类,则使用 CGLIB 生成子类代理。

3. 核心执行流程(事务是怎么运转的?) 当外部调用这个 Service 方法时,实际上是在调用代理对象的方法。代理对象会将执行权交给一个专门的切面类——TransactionInterceptor(事务拦截器)。拦截器的 invoke() 方法会执行一个标准的环绕通知(Around Advice),具体分为四步:

  • 第一步:获取/开启事务。拦截器会解析注解上的隔离级别(Isolation)和传播行为(Propagation),并调用 TransactionManager(事务管理器)在数据库层面真正开启一个事务。
  • 第二步:执行目标方法。通过反射机制(invocation.proceed()),调用我们自己写的、真实的 Service 业务逻辑。
  • 第三步:正常返回则提交。如果目标方法顺利执行完毕,没有抛出任何异常,拦截器会调用 commit() 方法,将数据永久持久化到数据库。
  • 第四步:捕获异常则回滚。如果目标方法抛出了异常,拦截器会进入 catch 逻辑。(高分关键点) 默认情况下,Spring 事务只在遇到 未检查异常(RuntimeExceptionError 时才会调用 rollback() 回滚;如果抛出的是普通的受检异常(如 Exception),默认是不会回滚的,除非在注解中显式配置 rollbackFor = Exception.class

我对这个事务@Transictional的理解就是 在service层里面用 然后就是主要是和数据库的变化有关 成功了就成功 要是有一方失败 就回滚 回滚用到了Transictionexcepter 就是用动态代理 搞个这个service方法的代理去看

✨职务:华夏大地区域代理人 | 熬夜秃头项目主理人 💳黑卡:校园一卡通全球辅导版持有者 📍地点:宇宙-银河系-地球-东北蹲分部 🥂生活方式:沉迷于廉价多巴胺 | 致力于在该醒的时候睡觉 🚫拒绝:拒绝早起 | 拒绝内卷| 拒绝借钱 简介:虽然我没钱,但我有时间;虽然我没才华,但我有脾气。
最后更新于 2026-03-08