声明式事务(@Transactional)的底层运行原理是什么?
Spring 声明式事务(@Transactional)的底层运行原理,可以高度概括为:Spring AOP(动态代理) + ThreadLocal + 数据库事务本身的机制。
它将底层的数据库连接管理、事务提交/回滚操作与业务代码解耦,让开发者只需关注业务逻辑。
以下是其底层运行原理的详细拆解:
一、 核心组件与基础机制
Spring AOP(动态代理)
当你在类或方法上添加@Transactional注解时,Spring IoC 容器在初始化该 Bean 时,会检测到这个注解。此时,Spring 不会把原始对象放入容器,而是利用 AOP 机制生成一个代理对象(Proxy)。- 如果类实现了接口,默认使用 JDK 动态代理。
- 如果类没有实现接口(或者在 Spring Boot 2.x 之后强制配置),则使用 CGLIB 动态代理。
TransactionInterceptor(事务拦截器)
代理对象中织入的“增强逻辑”就是TransactionInterceptor。当外部调用代理对象的方法时,请求会被这个拦截器拦截,从而在业务方法执行前后加入事务控制逻辑(即开启事务、提交/回滚事务)。ThreadLocal(线程绑定)
为了保证同一个事务中(如 Controller 调用 Service,Service 又调用多个 DAO),所有的数据库操作使用的是同一个数据库连接,Spring 底层使用TransactionSynchronizationManager(内部封装了ThreadLocal)将Connection绑定到当前线程上。
二、 底层执行的完整生命周期(6步走)
当客户端调用带有 @Transactional 的方法时,底层执行流程如下:
1. 拦截方法调用
外部调用该方法时,实际上调用的是代理对象。代理对象将请求转发给 TransactionInterceptor。
2. 解析事务属性
拦截器会读取 @Transactional 注解上的属性配置,比如:传播行为(Propagation)、隔离级别(Isolation)、超时时间(Timeout)、只读标记(ReadOnly)以及回滚规则(RollbackFor)。
3. 获取数据库连接并开启事务
- 拦截器调用
PlatformTransactionManager(如DataSourceTransactionManager)。 - 从数据库连接池(如 HikariCP、Druid)中获取一个
Connection。 - 核心动作:执行
connection.setAutoCommit(false),关闭数据库的自动提交机制,这标志着物理事务的开启。 - 将该
Connection与当前线程绑定(存入ThreadLocal中)。
4. 执行业务逻辑(目标方法)
拦截器通过反射(或 CGLIB 的 MethodProxy)调用实际的目标方法(即你写的业务代码)。此时,业务代码中所有的 DAO 操作,底层都会通过 ThreadLocal 拿到同一个 Connection 来执行 SQL。
5. 决定提交或回滚(核心判断)
目标方法执行完毕,流程回到拦截器中:
- 如果执行成功(没有抛出异常):
拦截器调用TransactionManager,执行connection.commit(),将数据持久化到数据库。 - 如果抛出异常:
拦截器会捕获异常,并判断该异常是否满足回滚条件(默认情况下,只有 RuntimeException 和 Error 会触发回滚,Checked Exception 不会引发回滚)。- 满足条件:执行
connection.rollback()。 - 不满足条件:依然执行
connection.commit()。
- 满足条件:执行
6. 资源清理
无论提交还是回滚,最后一步都会清理资源:
- 恢复连接的默认状态(
connection.setAutoCommit(true))。 - 将连接从
ThreadLocal中解绑。 - 将连接归还给数据库连接池。
三、 伪代码演示拦截器原理
为了更好地理解,可以看看 TransactionInterceptor 底层逻辑的极致精简版伪代码:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务管理器、事务属性等配置
PlatformTransactionManager tm = getTransactionManager();
TransactionAttribute txAttr = getTransactionAttribute();
// 2. 开启事务 (获取连接,设置 autoCommit = false,绑定到 ThreadLocal)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr);
Object retVal = null;
try {
// 3. 执行目标业务方法
retVal = invocation.proceed();
} catch (Throwable ex) {
// 4. 出现异常,判断是否需要回滚
if (txAttr.rollbackOn(ex)) {
tm.rollback(txInfo.getTransactionStatus());
} else {
tm.commit(txInfo.getTransactionStatus());
}
throw ex;
} finally {
// 6. 恢复线程状态,清理 ThreadLocal
cleanupTransactionInfo(txInfo);
}
// 5. 目标方法成功执行,提交事务
tm.commit(txInfo.getTransactionStatus());
return retVal;
}
四、 深入理解:为什么 @Transactional 会失效?
基于上述的底层原理(动态代理 和 拦截器异常捕获),我们可以非常清晰地解释为什么日常开发中事务经常会“失效”:
- 同类中方法内部调用(Self-Invocation)
- 现象:在同一个类中,方法 A(无事务)直接调用方法 B(有
@Transactional),事务不生效。 - 原理:直接调用
this.B()使用的是原生对象,没有经过代理对象,自然无法触发TransactionInterceptor的拦截。
- 现象:在同一个类中,方法 A(无事务)直接调用方法 B(有
- 方法非 public
- 现象:
@Transactional标注在private、protected或包级私有方法上失效。 - 原理:Spring AOP 规范要求只能对
public方法进行代理。拦截器底层在获取事务属性时,如果发现修饰符不是 public 会直接忽略。
- 现象:
- 异常被 try-catch 吞了
- 现象:方法内部抓住了异常没有抛出,事务没有回滚。
- 原理:
TransactionInterceptor是在外层通过catch捕获异常来决定是否回滚的。如果异常在业务代码内部被 catch 且没有throw,拦截器以为方法是正常结束的,于是执行了commit()。
- 抛出了 Checked Exception(如 IOException)
- 现象:抛出异常了,但事务却提交了。
- 原理:Spring 默认只在遇到
RuntimeException和Error时回滚。可以通过@Transactional(rollbackFor = Exception.class)来改变此默认行为。