前言:如果你在使用 Spring 框架,必定接触过
@Transactional注解。但你是否好奇过:写个注解就能自动提交和回滚,Spring 究竟在背后施了什么魔法?本文将带你扒开@Transactional的外衣,从宏观设计到底层源码 Debug,一步步搞透它的工作机制!
1. 业务场景:为什么我们需要事务?
在探究原理前,我们先看一个经典的转账场景 :
假设有一个 transfer() 方法,需要将资金从“发送方”转给“接收方”。代码逻辑主要有两步:
- 从发送方账户扣除金额,更新数据库。
- 给接收方账户增加金额,更新数据库。
痛点:这是完美的“快乐路径(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 代码块,它把你写的真实代码包在了中间:
- 准备工作(
txManager.getTransaction()):秘书在真正转账前,先跑去数据库那里喊一嗓子:“我要开启一个事务了啊!接下来发生的事情,全给我记到账本上!”(对应图里的 Before method execution)。 - 执行你的代码(
method.invoke()):这时候,秘书才把你从床上叫起来,让你去执行真正的“扣钱、加钱”逻辑(对应图里的 Target Method)。 - 顺利完成(
txManager.commit()):如果你的代码顺顺利利跑完了,什么错都没报。秘书就跑去告诉数据库:“老板事情办得很漂亮,把刚才的操作正式提交(Commit),永久保存!”(对应 After method execution (success))。 - 出了大问题(
catch ... txManager.rollback()):如果在执行你的代码时,突然系统断电了,或者报错抛异常了。秘书立马接管局面,跑去告诉数据库:“刚才全乱套了,不作数,全部给我回滚(Rollback),恢复到转账前的状态!”(对应 After method execution (failure))。
5. 源码级 Debug 走读:invoke() 方法内幕
为了证明上述理论,视频带我们直接杀入了 Spring 源码,拦截在 TransactionInterceptor 的核心方法——invoke() 上 。
当请求打进来时,invoke() 方法内部有几个极其关键的步骤:
- 获取事务属性:解析你的
@Transactional注解,看看你配置的隔离级别(Isolation)和传播机制(Propagation)是什么。 - 决定事务管理器 (Transaction Manager):Spring 会通过自动装配(Auto-Configuration)来推断当前该用哪个事务管理器 。比如如果你用的是 JPA,它就会为你悄悄注入
JpaTransactionManager。 - 获取/开启事务:根据传播机制,决定是加入现有事务还是新开一个事务。
- 执行目标方法 (
invocation.proceed()):这里才是真正去执行你写的转账逻辑。 - 异常与回滚:如果目标方法抛出异常,代码会进入
catch块,执行completeTransactionAfterThrowing()进行数据回滚。 - 成功与提交:如果方法正常返回,代码执行
commitTransactionAfterReturning()将数据永久落盘。
🔍 Debug 实战演示
视频最后通过 Postman/Swagger 模拟了两种真实场景的 Debug:
- 快乐路径 (Happy Scenario) :代码正常执行完
transfer里的所有数据库更新操作。没有报错,顺利跳过异常捕获区,最终走到commitTransactionAfterReturning()。打开数据库一看,发送方扣款 100,接收方加款 100,完美! - 灾难路径 (Failure Scenario) :作者在代码中强行抛出了一个“接收方服务器宕机”的异常。当代码执行到此处时,直接炸到
TransactionInterceptor的catch块。事务拦截器立即调用 rollback 相关逻辑。再次刷新数据库,发送方的钱原封不动,保证了资金安全!
秘书的 6 步工作流 (invoke() 方法大揭秘)
- 第 1 步:看老板的便签(获取事务属性) 秘书接到转账任务,先看一眼你贴在代码上的
@Transactional标签。秘书心想:“嗯,老板要求这是个最高级别的绝密任务(隔离级别),而且必须有保镖跟着(传播机制)。” - 第 2 步:找对口的财务主管(决定事务管理器) 公司里有很多财务(比如管 MySQL 的、管 Oracle 的)。秘书看了一眼公司的配置,发现你用的是 JPA 技术,于是立马喊来了
JpaTransactionManager(JPA 财务主管)来负责记账。 - 第 3 步:翻开新的账本(获取/开启事务) 准备工作就绪,秘书对财务主管说:“铺开一张全新的白纸,准备开始记账!从现在起的操作,全部算一笔账!”
- 第 4 步:老板亲自下场干活(执行目标方法
invocation.proceed()) 这是最核心的一步!这里代表着真正的你出场了。秘书说:“老板,舞台搭好了,你可以开始转账了!” 于是系统开始执行你写的“扣 A 账户 100 块,加 B 账户 100 块”的代码。 - 第 5 步 或 第 6 步:看老板干活的结果(异常与回滚 / 成功与提交) 干活只有两种结果,于是就有了所谓的**“快乐路径”和“灾难路径”**:
🌟 场景一:快乐路径 (Happy Scenario) -> 一切顺利
- 你(老板)把代码写得毫无破绽,A 扣钱成功,B 加钱成功。
- 整个过程行云流水,没有报错。
- 秘书在旁边一看,激动地喊出那个长长的绝招:
commitTransactionAfterReturning()! - 大白话:秘书对财务主管说:“老板干得漂亮,赶紧把刚才写在草稿纸上的账,**正式抄写(Commit)**到公司大账本里,盖章生效!”(最终数据库被完美更新)。
💥 场景二:灾难路径 (Failure Scenario) -> 突然翻车
- 你(老板)刚把 A 的钱扣掉,正准备给 B 加钱时,突然停电了,或者 B 的银行卡被冻结了!
- 你的代码瞬间“炸毛”了,抛出了一个异常(Exception)。
- 秘书反应极快,眼疾手快地用一个叫
catch的安全网把你接住,并喊出另一个绝招:completeTransactionAfterThrowing()! - 大白话:秘书对财务主管说:“快快快,刚才老板转账搞砸了!把刚才那张写了一半的草稿纸直接撕掉(Rollback 回滚),当做什么都没发生过!”
- 结果就是: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 事务只在遇到 未检查异常(RuntimeException) 和Error时才会调用rollback()回滚;如果抛出的是普通的受检异常(如Exception),默认是不会回滚的,除非在注解中显式配置rollbackFor = Exception.class。
我对这个事务@Transictional的理解就是 在service层里面用 然后就是主要是和数据库的变化有关 成功了就成功 要是有一方失败 就回滚 回滚用到了Transictionexcepter 就是用动态代理 搞个这个service方法的代理去看
Comments NOTHING