理解 Spring 框架的声明式事务实现 (Understanding the Spring Framework’s Declarative Transaction Implementation)
仅仅告诉你给类加上 @Transactional 注解、在配置中添加 @EnableTransactionManagement 并在预期内认为它能正常工作是不够的。为了提供更深入的理解,本节将结合事务相关的议题,解释 Spring 框架声明式事务基础设施的内部运行机制。
关于 Spring 框架声明式事务支持,最需要掌握的重要概念是:这种支持是通过 AOP 代理实现的,并且事务通知是由元数据(目前是基于 XML 或注解)驱动的。AOP 与事务元数据的结合产生了一个 AOP 代理,它使用 TransactionInterceptor(事务拦截器)配合适当的 TransactionManager 实现,围绕方法调用来驱动事务。
注意
Spring AOP 的内容在 AOP 章节 中有涵盖。
Spring 框架的 TransactionInterceptor 为命令式(Imperative)和响应式(Reactive)编程模型都提供了事务管理。拦截器通过检查方法返回值类型来检测所需的事务管理风格。返回响应式类型(如 Publisher 或 Kotlin Flow 及其子类型)的方法适用于响应式事务管理。所有其他返回类型(包括 void)都使用命令式事务管理的路径。
事务管理的风格决定了需要哪种事务管理器。命令式事务需要 PlatformTransactionManager,而响应式事务则使用 ReactiveTransactionManager 实现。
注意
@Transactional 通常与由 PlatformTransactionManager 管理的绑定到线程的事务配合工作,将事务暴露给当前执行线程内所有的数据访问操作。注意:这不会传播到该方法内部新启动的线程。
由 ReactiveTransactionManager 管理的响应式事务使用 Reactor Context 而不是线程局部(thread-local)属性。因此,所有参与的数据访问操作都需要在同一个响应式流水线的同一个 Reactor Context 中执行。
当配置了 ReactiveTransactionManager 时,所有被划分事务边界的方法都预期返回一个响应式流水线(Pipeline)。void 方法或常规返回类型需要关联到常规的 PlatformTransactionManager,例如通过对应 @Transactional 声明的 transactionManager 属性来指定。
下图展示了在事务代理上调用方法的概念图:

补充教学
1. 事务拦截器 (TransactionInterceptor) 的角色
你可以把 TransactionInterceptor 想象成一个“环绕通知” (Around Advice)。
- 前置动作:在你的业务方法执行前,它会查找元数据(即
@Transactional的配置),从TransactionManager获取一个事务。 - 异常处理:它会把你的业务代码包裹在一个巨大的
try-catch块中。如果捕获到符合回滚规则的异常,它会命令事务管理器执行rollback。 - 后置动作:如果业务方法正常执行完毕,它会命令事务管理器执行
commit。
2. 为什么会有两种实现路径?
- 命令式路径 (JDBC/JPA/MyBatis):代码是同步执行的。Spring 可以放心地把事务状态存入当前线程。
- 响应式路径 (R2DBC/MongoDB Reactive):代码是异步执行的,会在不同的线程间切来切去。如果使用线程局部变量,事务会瞬间断开。因此,Spring 必须利用响应式库(如 Project Reactor)自带的类似“线程上下文”的概念——Context,将事务标识随流传递。
3. 经典的“嵌套调用”失效陷阱
这是理解 AOP 代理机制最实用的地方。
@Service
public class MyService {
public void outerMethod() {
// 直接调用内部方法,不会经过代理对象
innerMethod();
}
@Transactional
public void innerMethod() {
// 这里的事务不会生效!
}
}原因:Spring 的事务增强是加在“代理对象”上的,而不是加在你的“原始类”上的。当你调用 outerMethod 时,你可能是在调用代理。但 outerMethod 内部调用 innerMethod 时,它是通过 this 指针直接调用的,并没有经过代理对象的拦截逻辑,因此 TransactionInterceptor 没机会介入。
4. 事务与多线程
文档中提到事务不会传播到新线程。这是一个非常重要的警告。
- 如果你在
@Transactional方法里手动启动了一个new Thread(() -> { ... })或使用了@Async,那个新线程里的数据库操作不属于父线程的事务。 - 后果:新线程的操作要么没有事务,要么会开启一个完全独立的事务。这往往会导致数据不一致。