1. 完整回答
Spring 事务是 Spring 提供的一套统一事务管理机制,主要用于保证一组数据库操作的原子性和数据一致性。
比如转账业务,需要同时完成账户扣款和账户入账。如果扣款成功,但入账失败,就会产生数据不一致。通过事务可以让这两个操作处于同一个事务中,要么全部提交,要么一起回滚。
Spring 最常用的是声明式事务,通过 @Transactional 注解实现。
底层主要依赖 AOP 动态代理和事务管理器。当外部调用进入代理对象时,事务拦截器会根据传播行为判断是否创建新事务,或者加入已有事务。随后执行目标方法,方法正常完成就尝试提交事务,出现符合回滚规则的异常就回滚事务。
Spring 还提供了传播行为、隔离级别、回滚规则、超时和只读属性,用于控制事务之间的关系、并发访问以及异常处理。
其中默认传播行为是 REQUIRED,有事务就加入,没有就创建;默认遇到 RuntimeException 或 Error 会回滚,受检异常需要额外配置回滚规则。
实际开发中,事务通常放在 Service 层,用于订单创建、库存扣减、账户余额变更等涉及多次数据库操作的业务。
需要注意的是,Spring 默认通过代理管理事务,同类内部方法调用可能绕过代理,而且普通本地事务也无法自动保证跨线程、跨服务操作的一致性。
2. 核心原理与关键概念
2.1 核心原理
Spring 声明式事务的核心是 AOP + TransactionInterceptor + PlatformTransactionManager。
完整执行流程:
外部调用方法 → AOP 代理拦截 → 获取事务配置 → 创建或加入事务 → 执行业务方法 → 提交或回滚 → 清理事务资源
具体过程:
-
创建代理对象:Spring 启用事务管理后,为符合条件的 Bean 创建代理对象。
-
拦截方法调用:外部调用进入代理,
TransactionInterceptor读取@Transactional的事务属性。 -
获取事务:事务管理器根据传播行为决定创建事务、加入已有事务或暂停事务。
-
绑定资源:对于常规 JDBC 事务,Spring 将数据库连接等事务资源绑定到当前线程,保证参与事务的数据访问操作使用对应资源。
-
执行与结束:执行目标方法,根据执行结果和回滚规则提交或回滚,最后恢复或清理事务资源。
实际的数据库提交、回滚和隔离控制由底层数据库及相关事务资源完成。
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 事务代理。
主要有以下几种情况:
-
同类内部调用:一个方法直接调用同一个类中的事务方法,会绕过代理对象。
-
对象没有交给 Spring 管理:通过
new创建的普通对象,不会自动获得 Spring 事务代理。 -
方法无法被代理拦截:例如
private方法,以及使用 CGLIB 时的final方法。 -
事务管理配置不正确:例如未启用事务管理,或者事务管理器与实际数据源不匹配。
另外,还需要区分事务失效和事务未回滚。受检异常没有配置回滚规则,或者异常被方法内部捕获,也可能造成数据没有回滚,但事务本身已经生效。
实际开发中,通常把事务放在 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 层,尽量缩短事务范围,同时注意同类内部调用绕过代理、异常被捕获以及事务无法自动跨线程和跨服务传播的问题。