← 返回文章

Spring 事务

发布于 2026/10/21

1. 完整回答

Spring 事务是 Spring 提供的一套统一事务管理机制,主要用于保证一组数据库操作的原子性和数据一致性。

比如转账业务,需要同时完成账户扣款和账户入账。如果扣款成功,但入账失败,就会产生数据不一致。通过事务可以让这两个操作处于同一个事务中,要么全部提交,要么一起回滚。

Spring 最常用的是声明式事务,通过 @Transactional 注解实现。

底层主要依赖 AOP 动态代理和事务管理器。当外部调用进入代理对象时,事务拦截器会根据传播行为判断是否创建新事务,或者加入已有事务。随后执行目标方法,方法正常完成就尝试提交事务,出现符合回滚规则的异常就回滚事务。

Spring 还提供了传播行为、隔离级别、回滚规则、超时和只读属性,用于控制事务之间的关系、并发访问以及异常处理。

其中默认传播行为是 REQUIRED,有事务就加入,没有就创建;默认遇到 RuntimeException 或 Error 会回滚,受检异常需要额外配置回滚规则。

实际开发中,事务通常放在 Service 层,用于订单创建、库存扣减、账户余额变更等涉及多次数据库操作的业务。

需要注意的是,Spring 默认通过代理管理事务,同类内部方法调用可能绕过代理,而且普通本地事务也无法自动保证跨线程、跨服务操作的一致性。

2. 核心原理与关键概念

2.1 核心原理

Spring 声明式事务的核心是 AOP + TransactionInterceptor + PlatformTransactionManager。

完整执行流程:

外部调用方法 → AOP 代理拦截 → 获取事务配置 → 创建或加入事务 → 执行业务方法 → 提交或回滚 → 清理事务资源

具体过程:

  1. 创建代理对象:Spring 启用事务管理后,为符合条件的 Bean 创建代理对象。

  2. 拦截方法调用:外部调用进入代理,TransactionInterceptor 读取 @Transactional 的事务属性。

  3. 获取事务:事务管理器根据传播行为决定创建事务、加入已有事务或暂停事务。

  4. 绑定资源:对于常规 JDBC 事务,Spring 将数据库连接等事务资源绑定到当前线程,保证参与事务的数据访问操作使用对应资源。

  5. 执行与结束:执行目标方法,根据执行结果和回滚规则提交或回滚,最后恢复或清理事务资源。

实际的数据库提交、回滚和隔离控制由底层数据库及相关事务资源完成。

2.2 关键概念

概念 作用
@Transactional 声明事务边界和事务属性
AOP 动态代理 拦截方法调用,在业务方法前后执行事务逻辑
TransactionInterceptor 根据事务配置协调事务的开启、提交和回滚
PlatformTransactionManager 同步事务管理的统一接口,提供获取事务、提交和回滚能力
传播行为 propagation 决定方法如何参与已有事务
隔离级别 isolation 控制并发事务之间的数据可见性
回滚规则 rollbackFor 指定哪些异常需要触发事务回滚

常见事务管理器:

  • DataSourceTransactionManager:管理 JDBC 数据源事务。

  • JpaTransactionManager:管理 JPA 持久化事务。

  • JtaTransactionManager:对接 JTA 事务基础设施,可协调受支持的多个事务资源。

3. 高频面试追问

3.1 Spring 事务为什么使用 AOP 动态代理?

主要是为了统一管理事务边界,并降低业务代码和事务管理代码之间的耦合。

Spring 使用 AOP,在目标方法执行前开启或加入事务,在方法执行结束后提交或回滚。业务代码只需要声明事务属性,无需反复编写数据库连接、提交和回滚逻辑。

Spring AOP 主要有两种代理方式:

  • JDK 动态代理:基于接口创建代理。

  • CGLIB 代理:通过生成目标类的子类实现代理。

具体采用哪种方式取决于代理配置及目标类结构。

无论使用哪种代理,默认代理模式下都需要通过代理对象调用方法,事务拦截逻辑才有机会执行。

3.2 @Transactional 在哪些情况下会失效?

最常见的原因是方法调用没有经过 Spring 事务代理。

主要有以下几种情况:

  1. 同类内部调用:一个方法直接调用同一个类中的事务方法,会绕过代理对象。

  2. 对象没有交给 Spring 管理:通过 new 创建的普通对象,不会自动获得 Spring 事务代理。

  3. 方法无法被代理拦截:例如 private 方法,以及使用 CGLIB 时的 final 方法。

  4. 事务管理配置不正确:例如未启用事务管理,或者事务管理器与实际数据源不匹配。

另外,还需要区分事务失效和事务未回滚。受检异常没有配置回滚规则,或者异常被方法内部捕获,也可能造成数据没有回滚,但事务本身已经生效。

实际开发中,通常把事务放在 Spring 管理的 Service 的 public 方法上,并通过其他 Bean 调用。

继续追问:同类内部调用一定没有事务吗?

不一定。

如果外层方法已经通过代理开启事务,内部调用的方法仍然执行在外层事务上下文中。

只是内部方法自己的 @Transactional 配置不会被重新拦截处理。

例如外层使用 REQUIRED,内部标注 REQUIRES_NEW,直接内部调用时不会开启独立的新事务,相关数据库操作仍可能参与外层事务。

3.3 Spring 有哪些事务传播行为?

Spring 一共有七种传播行为,主要用于决定当前方法遇到已有事务时应该如何处理。

其中最常用的是:

  • REQUIRED:默认传播行为,有事务就加入,没有就创建新事务,适合大多数业务。

  • REQUIRES_NEW:始终开启独立事务,如果已有事务,会先暂停外层事务。适合需要独立提交的业务。

  • NESTED:在已有事务中通过保存点实现局部回滚,适用于支持保存点的事务管理器。

另外四种分别是:

  • SUPPORTS:有事务就加入,没有就以非事务方式执行。

  • MANDATORY:必须存在事务,否则抛异常。

  • NOT_SUPPORTED:以非事务方式执行,必要时暂停已有事务。

  • NEVER:要求当前没有事务,存在事务就抛异常。

实际业务中,通常优先使用 REQUIRED,只有需要独立提交或者局部回滚时,才考虑其他传播行为。

3.4 为什么内层事务异常被捕获,外层仍然可能回滚?

这是因为 REQUIRED 传播行为下,内外层事务共享同一个物理事务。

例如外层 A 方法开启事务,调用另一个 Bean 的 B 方法,B 使用默认的 REQUIRED。

执行过程是:

A 开启事务 → B 加入 A 的事务 → B 抛出运行时异常 → B 的事务拦截器将事务标记为 rollback-only → A 捕获异常并正常返回 → A 尝试提交。

由于事务已经被标记为只能回滚,最终无法正常提交,Spring 通常会抛出 UnexpectedRollbackException。

这个机制可以防止外层代码误认为事务已经成功提交。

如果业务确实允许内层失败、外层继续成功,可以根据场景使用 REQUIRES_NEW 隔离事务,或者调整异常处理和事务边界。

需要注意,REQUIRES_NEW 的内层异常如果继续向外抛出,也可能触发外层事务回滚。

3.5 Spring 事务遇到异常一定会回滚吗?

不一定,是否回滚主要取决于异常类型、回滚规则,以及异常有没有被事务拦截器感知。

Spring 默认规则是:

  • RuntimeException:回滚。

  • Error:回滚。

  • 受检异常,例如 IOException:默认不会触发回滚。

如果希望受检异常也回滚,可以配置:

@Transactional(rollbackFor = Exception.class)

另外,如果异常在事务方法内部被 try-catch 捕获,并且正常返回,Spring 通常会尝试提交事务。

如果捕获异常后仍然需要回滚,可以重新抛出符合规则的异常,或者通过 setRollbackOnly() 显式标记回滚。

实际业务中,会根据异常是否要求撤销数据库变更来配置回滚规则,避免异常被吞掉后产生部分数据提交的问题。

3.6 Spring 事务的隔离级别有哪些?如何选择?

Spring 提供了五种隔离级别配置,其中 DEFAULT 表示使用底层数据库默认隔离级别。

其他四种对应常见的数据库事务隔离级别:

隔离级别 主要特点
READ_UNCOMMITTED 允许读取未提交数据,可能出现脏读
READ_COMMITTED 避免脏读,但可能出现不可重复读
REPEATABLE_READ 避免脏读和不可重复读,标准语义下仍需考虑幻读
SERIALIZABLE 提供最严格的隔离语义,并发开销通常更高

这里涉及三个现象:

  • 脏读:读取到其他事务尚未提交的数据。

  • 不可重复读:同一事务两次读取同一行,结果发生变化。

  • 幻读:同一事务按照相同条件查询,得到的符合条件的记录集合发生变化。

实际使用时,通常先遵循数据库默认隔离级别,再根据业务一致性要求调整。

例如普通业务查询可能使用读已提交;需要稳定读取结果时,可以结合可重复读、锁或其他并发控制机制。

不同数据库采用的 MVCC 和锁机制存在差异,具体行为要结合数据库实现判断。事务加入已有事务时,隔离级别通常沿用外层事务的配置。

3.7 @Transactional(readOnly = true) 有什么作用?能禁止写入吗?

readOnly = true 主要用于声明当前事务以读取为主,并向底层事务系统提供优化提示。

例如在 JPA 和 Hibernate 场景下,可以通过调整刷新策略、减少脏检查等方式降低部分开销。

对于 JDBC,也可以将只读属性传递给数据库连接。

但这个属性不能保证所有数据库写操作都会被禁止。

是否拒绝写入取决于数据库、驱动以及事务管理器的具体实现。

另外,如果只读方法加入了一个已有的读写事务,默认情况下通常沿用外层事务属性。

因此,readOnly = true 适合纯查询业务,但不能代替数据库账号权限等强制只读控制。

3.8 Spring 事务能跨线程或异步方法传播吗?

对于常规声明式事务,事务上下文默认绑定在当前线程上,无法自动传播到新线程。

Spring 通过 TransactionSynchronizationManager 管理当前线程关联的事务资源。

如果在事务方法中开启新线程,或者通过 @Async 执行异步任务,新线程不会自动继承原线程的事务上下文。

因此,主线程回滚,不能直接让已经执行的异步数据库操作一起回滚。

如果异步任务需要事务,可以让异步线程通过 Spring 代理开启自己的事务,再通过消息、补偿或者其他机制协调最终结果。

补充一点,Spring 响应式事务使用 Reactor Context 传播事务上下文,机制与普通基于线程绑定的事务有所区别。

3.9 实际项目中,Spring 事务应该如何设计?

一般会把事务边界放在 Service 层的完整业务操作上,确保需要原子性的一组数据库修改处于同一个事务中。

例如创建订单时,需要保存订单并扣减库存,如果两者使用同一个事务资源,就可以通过 REQUIRED 保证任何一步失败时整体回滚。

同时,事务范围应尽量短,避免在事务中执行耗时的 HTTP 调用、远程服务请求或者长时间等待,因为这些操作可能延长连接占用和数据库锁持有时间。

对于订单创建后发送消息的场景,可以使用 @TransactionalEventListener 在事务成功提交后处理事件。

但如果要求消息可靠投递,仅依赖提交后回调仍有进程崩溃导致事件丢失的风险。通常会使用 Outbox 本地消息表,将业务数据和待发送消息一起提交,再异步投递,并结合幂等和重试实现最终一致性。

继续追问:多个数据库或者多个微服务,使用一个 @Transactional 能保证整体回滚吗?

通常不能。

普通本地事务只管理对应事务管理器所控制的资源。

如果涉及多个数据库或远程服务,单个本地事务无法自动协调所有参与方。

这类场景需要根据一致性要求选择 JTA/XA 分布式事务,或者使用 Saga、Outbox 等最终一致性方案。

4. 易混淆概念对比

4.1 REQUIRED、REQUIRES_NEW、NESTED

对比项 REQUIRED REQUIRES_NEW NESTED
已有事务时 加入外层事务 暂停外层,开启新事务 在外层事务中建立保存点
物理事务 共享一个 相互独立 共享一个
内层回滚 会影响整个共享事务 可独立回滚 可回滚到保存点
内层提交 随外层最终提交 可独立提交 最终仍依赖外层提交
常见场景 订单与库存原子操作 独立审计记录 支持保存点的局部回滚

NESTED 通常依赖 JDBC 保存点以及对应事务管理器的支持,不能假定所有 JPA 或 JTA 场景都能使用。

4.2 事务传播行为 vs 事务隔离级别

对比项 传播行为 隔离级别
核心问题 方法如何参与已有事务 并发事务之间如何访问数据
关注方向 事务之间的调用关系 数据可见性和并发一致性
典型配置 REQUIRED、REQUIRES_NEW READ_COMMITTED、REPEATABLE_READ
常见用途 控制是否创建独立事务 控制脏读、不可重复读等现象

4.3 flush vs commit

对比项 flush commit
含义 将 ORM 持久化上下文中的待执行变更同步到数据库 提交数据库事务
是否代表提交成功 否 成功后事务变更正式提交
是否还能回滚 通常可以 已成功提交的变更无法通过原事务回滚

在 JPA/Hibernate 中,flush 可能使 SQL 提前执行,但只要事务尚未提交,相关数据库变更通常仍然可以回滚。

4.4 声明式事务 vs 编程式事务

对比项 声明式事务 编程式事务
实现方式 @Transactional TransactionTemplate 等
事务边界 由代理按照配置管理 由代码显式控制
优势 业务代码简洁,适合常规场景 适合复杂或动态事务边界
局限 受到代理调用机制影响 事务控制代码相对更多

4.5 本地事务 vs 分布式事务

对比项 本地事务 分布式事务
管理范围 单个事务资源或本地事务管理范围 多个独立事务资源或服务
主要目标 保证本地操作原子性 协调多个参与方的一致性
常见方案 Spring + JDBC/JPA 事务管理器 XA/JTA、Saga 等
典型场景 单库订单和库存修改 跨库操作、跨服务业务流程

5. 总结

Spring 事务主要用于保证一组数据库操作的原子性和数据一致性。它通过 AOP 动态代理、事务拦截器和事务管理器统一控制事务边界,根据传播行为决定创建还是加入事务,并按照回滚规则完成提交或回滚。默认使用 REQUIRED 传播行为,遇到运行时异常或 Error 会回滚。实际开发中,通常把事务放在 Service 层,尽量缩短事务范围,同时注意同类内部调用绕过代理、异常被捕获以及事务无法自动跨线程和跨服务传播的问题。