Spring 事务
Spring 事务通过拦截器和事务管理器,控制一组数据库操作何时提交或回滚。本文以 Spring 6.2 的 JDBC 本地事务为主,先沿代理调用说明连接怎样参与事务,再介绍传播、隔离与回滚规则。最后说明提交失败和提交后事件的处理边界。
1. 面试核心
先记住这几条,再读正文- 事务拦截。
TransactionInterceptor在方法调用前开启或加入事务,方法退出时根据结果和回滚规则请求提交或回滚。卡 · 事务注解与代理 - 线程绑定连接。 Spring JDBC 事务将连接绑定到当前线程,通过 Spring 获取同一数据源连接的操作会复用该连接。
- 默认回滚规则。 默认配置下,传出方法的
RuntimeException与Error触发回滚,受检异常可用rollbackFor指定。 - 独立事务与保存点。
REQUIRES_NEW挂起外层并创建独立事务,NESTED在已有 JDBC 事务中用保存点实现局部回滚。卡 · REQUIRES_NEW - 共享回滚标记。 内层
REQUIRED标记 rollback-only 后,外层提交会抛UnexpectedRollbackException。卡 · REQUIRED
2. 事务注解与代理
在默认代理模式下,容器识别 @Transactional 后,为 Bean 创建包含事务拦截器的代理。外部调用进入这份代理时,注解声明的事务规则才参与执行:
@Transactional
public Order create(CreateOrder command) {
Order order = repository.save(command.toOrder());
inventoryRepository.reserve(order.items());
return order;
}
示例的两个 repository 使用同一受管数据源,并通过 Spring 获取连接。外部调用进入事务代理后,保存订单与预留库存两次写入会参与同一事务。
注解可以声明传播方式、隔离级别、超时、只读提示和回滚规则。事务管理器负责把这些声明转换为资源操作,因此具体支持范围还取决于管理器与数据库。
3. 事务拦截器
事务拦截器把方法执行包在事务边界内。它先读取事务属性 TransactionAttribute,再交给事务管理器选择新建还是加入事务:
进入代理
→ 解析 TransactionAttribute
→ 选择 TransactionManager
→ 获取或加入事务
→ 执行目标方法
→ 正常返回:请求提交
→ 异常退出:按规则提交或回滚
→ 清理事务上下文与资源
新建事务的调用边界负责实际提交。如果内层加入的是已有事务,它返回时只结束自己的逻辑事务范围。这里的逻辑范围指当前方法的事务调用边界,底层数据库事务仍需等外层结束。
PlatformTransactionManager 用于命令式事务,JDBC、JPA 和 JTA 使用相应实现。JTA 用于协调多个资源的全局事务。响应式事务则使用 ReactiveTransactionManager,由 Reactor Context 携带事务上下文。Reactor Context 是响应式订阅链传递上下文的机制,本文的线程绑定描述适用于 JDBC 路径。
4. 连接与事务上下文
两次数据库操作要参加同一 JDBC 事务,就需要取得同一条事务连接。JDBC 事务管理器把连接关联到当前线程和数据源,JdbcTemplate 等组件再通过 Spring 取得它。因此,同一线程内的参与操作能够复用连接。事务结束后,管理器提交或回滚,再将连接归还连接池。
直接调用普通 DataSource.getConnection() 会绕开 Spring 的资源查找,取得的连接就可能属于另一项事务。需要操作底层连接时,可使用 DataSourceUtils;事务感知数据源代理也能协调获取已有连接。
新线程有独立上下文,需要通过事务入口建立自己的事务。把任务交给另一个线程后,原线程的连接绑定不会自动跟随。
多数据源场景可用 @Transactional(transactionManager="ordersTxManager") 明确选择管理器。单个本地管理器只管理其负责的数据源,因此独立数据源上的写入还需单独确定事务边界。
5. 提交失败
目标方法正常返回,只说明业务代码执行完毕,事务仍需实际提交。数据库约束检查或连接故障都可让提交失败。使用 JPA 时,flush 会把尚未发送的持久化修改同步到数据库,也可能在提交前失败。因此,调用方应以事务代理的最终返回结果判断成功。
JTA 全局事务还可能在协调多个资源提交时失败,相关机制见 2PC 与 TCC。
远程调用与本地提交分属不同资源,单个 JDBC 事务无法保证它们一起成功。需要可靠发送消息时,可把待发事件写入 Outbox,即与业务数据在同一本地事务中提交的事件表。后台发布器再读取并投递,接收方用幂等键处理重复消息。
可靠消息机制见 Transactional Outbox 与 CDC。
6. 事务边界
事务边界应覆盖必须共同提交的数据库操作。边界过小,会把订单和库存等关联状态拆开;边界过大,则长期占用连接和锁,并增加回滚成本。
一次应用服务调用可以承载完整的数据库决策与更新,依次完成以下三个阶段:
- 读取作出业务决策所需的状态。
- 检查业务规则,执行需要共同提交的数据库更新。
- 提交事务,将结果返回调用方。
耗时网络调用应移出本地事务,否则远程等待期间连接和锁仍被占用。前后步骤通过可恢复状态连接,并明确重试和补偿责任。
7. 编程式事务
需要在循环中逐批提交、限定重试范围,或绕开代理自调用限制时,可使用 TransactionTemplate。它通过回调明确标出哪些代码应在事务内执行:
return transactionTemplate.execute(status -> repository.save(order));
回调返回后,模板按规则请求提交;异常退出时则处理回滚。传播方式仍由模板设置决定,因此加入已有事务时仍需等待外层提交。
编程式事务让边界直接出现在代码中,代价是业务代码显式依赖 Spring 事务 API。选择声明式还是编程式,应看哪种方式能更清楚地表达提交范围。
8. 传播、隔离与回滚
理解事务结果时,需要区分三类配置:
- 传播方式决定方法进入时怎样处理已有事务。
- 隔离级别控制数据库并发读写时能看到哪些数据。
- 回滚规则决定异常是否触发回滚。
它们作用在不同环节,因此需要分别配置和验证。
8.1 REQUIRED
REQUIRED 是默认传播方式,用于让多个方法共同完成一次数据库提交。有事务时加入,没有事务时新建。内外方法各有自己的逻辑边界,但共享同一个物理事务,也就是数据库中实际提交或回滚的那项事务。
内层代理收到匹配回滚规则的异常时,会将共享事务标为 rollback-only,即只允许回滚。外层即使捕获异常,这个标记仍会保留。等外层请求提交时,管理器就会回滚,并抛出 UnexpectedRollbackException,让调用方知道提交实际失败。
若异常在内层方法内部被捕获,拦截器只看到正常返回。没有其他回滚标记时,事务按正常路径提交。
8.2 REQUIRES_NEW
需要让某次写入独立提交时,可以使用 REQUIRES_NEW。它挂起外层事务,再用另一条连接开启独立事务,例如记录必须独立保留的审计事件:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordAudit(AuditEvent event) {}
内层可以独立提交或回滚,完成后释放自己的锁与连接。如果内层已提交,外层随后回滚也会保留这条审计记录。因此,业务需要接受该记录单独存在,或安排后续补偿。
外层挂起期间仍持有连接和锁,内层还要额外申请连接。如果多个线程都持有外层连接、又等待内层连接,连接池耗尽就会阻塞所有参与线程。容量规划需要覆盖这部分额外需求。若内层等待外层已持有的锁,还会形成跨事务等待。
8.3 NESTED
需要局部撤销一段操作、但仍由外层统一提交时,可以使用 NESTED。DataSourceTransactionManager 会在已有 JDBC 事务中建立保存点,也就是供局部回滚使用的位置。内层失败时回滚到这个位置,外层处理异常后便可继续执行并尝试提交。
外层最终回滚时,内层写入也被撤销。没有外层事务时,NESTED 按 REQUIRED 新建事务。
保存点需 JDBC 驱动支持。JPA 持久化上下文中的对象状态还需单独考虑。应按具体事务管理器确认支持范围。
8.4 其他传播方式
有些方法需要强制已有事务,或明确在事务外执行。以下传播方式分别约束方法在有事务和无事务时的行为:
| 传播方式 | 已有事务 | 没有事务 |
|---|---|---|
SUPPORTS | 加入 | 非事务执行 |
MANDATORY | 加入 | 抛异常 |
NOT_SUPPORTED | 挂起 | 非事务执行 |
NEVER | 抛异常 | 非事务执行 |
挂起能力由事务管理器提供。非事务执行时,数据库写入仍需按连接的自动提交设置判断。传播方式组合复杂时,应通过集成测试验证实际提交和回滚结果。
8.5 隔离级别
隔离级别决定数据库事务能看到哪些并发修改。Isolation.DEFAULT 沿用数据库默认值,显式声明的隔离和超时则应用于新建事务。加入已有事务时,内层沿用外层设置,因为双方仍在同一个物理事务中。
调用 AbstractPlatformTransactionManager.setValidateExistingTransaction(true) 可开启严格校验。内层声明的隔离级别与外层不一致时,管理器会拒绝参与。它也会拒绝让读写内层加入只读外层,便于提前发现不兼容的声明。
readOnly=true 是资源优化提示。JDBC 管理器可用 setEnforceReadOnly(true) 加强只读执行。具体支持仍依赖数据库;权限约束应由数据库授权承担。隔离级别只控制数据库中的并发可见性,远程服务和缓存的一致性还需各自的协调机制。
8.6 回滚规则
回滚规则用于把方法失败映射为事务结果。默认配置下,传出方法的 RuntimeException 和 Error 会触发回滚,受检异常则按正常提交处理。需要让业务受检异常也触发回滚时,可显式声明:
@Transactional(rollbackFor = PaymentException.class)
示例将 PaymentException 及其子类纳入回滚范围。异常需要到达事务拦截器;如果目标方法内部已捕获异常,应重新抛出或显式设置 rollback-only。业务还要把失败结果传给调用方,避免让调用方误以为操作成功。
noRollbackFor 可声明哪些异常按提交处理。多个规则同时匹配时,与异常类型继承距离更近的规则优先。
Spring 6.2 可用 @EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS) 更改全局默认。该设置将受检异常也纳入默认回滚。局部规则仍可覆盖默认行为。
9. 提交后事件
有些后续操作需要等数据库提交成功后再执行,可以使用 @TransactionalEventListener。它默认在 AFTER_COMMIT 阶段触发,也就是事务提交完成之后。发布事件时若没有事务,默认跳过监听器;设置 fallbackExecution=true 则允许无事务执行。
监听器执行时仍能访问绑定资源,但原事务已经结束,继续使用它也不会再提交一次。若监听器需要写库,应通过另一个代理方法开启 REQUIRES_NEW,为新写入建立独立提交边界。
普通事务事件保存在进程内存中。提交后进程崩溃会丢失尚未执行的通知。要求可靠投递时,应使用持久化 Outbox 等机制。
10. 常见问题
10.1 本地事务与远程写入
本地事务只管理所属资源。远程服务已经完成的写入需用补偿或跨服务事务协议处理。
10.2 只读事务的约束
readOnly=true 向资源传递优化意图。JDBC 的强制只读设置和数据库权限提供更明确的限制。
10.3 独立事务的回滚范围
内层 REQUIRES_NEW 提交后独立保留。外层回滚只撤销外层事务的写入。
10.4 保存点回滚后的外层事务
回到保存点后,外层可继续尝试提交。前提是异常已处理,且数据库事务仍可用。
11 · 面试题卡片
11.1 @Transactional 从调用到提交经历什么
出现公司:美团、阿里云
考察重点:沿代理调用解释事务属性解析、连接复用和实际提交,并区分方法返回与事务成功。
参考回答
调用进入事务代理后,拦截器先建立事务范围,再执行目标方法,最后按执行结果请求提交或回滚。
它从 @Transactional 解析事务属性,并选择对应管理器。管理器新建或加入事务,JDBC 路径还会按线程绑定连接,供参与操作复用。
方法正常返回时,拦截器请求提交;异常退出时,则按回滚规则处理。实际提交仍会因约束或连接故障失败,事务结束后再清理上下文并归还连接。加入已有事务的内层方法返回时,只结束逻辑范围,实际提交仍由外层负责。
这条链路只覆盖管理器负责的资源。远程写入和消息发送需要额外的一致性机制,例如持久化事件与幂等处理。
11.2 REQUIRES_NEW 与 NESTED 有什么区别
出现公司:阿里云、美团
考察重点:说明独立事务与保存点分别怎样处理内外层回滚,并判断连接池容量和驱动支持带来的限制。
相关内容:REQUIRES_NEW和NESTED。
参考回答
REQUIRES_NEW 使用独立物理事务,NESTED 则在已有 JDBC 事务中使用保存点。
前者挂起外层后另取连接,内层可以独立提交或回滚,完成时释放自己的资源。内层已提交的结果会保留,即使外层后来回滚。
NESTED 内层失败时可回到保存点,让外层决定是否继续。因为双方仍共享同一物理事务,外层最终回滚也会撤销内层的写入。
REQUIRES_NEW 需要额外连接,应评估连接池耗尽风险。NESTED 则要求事务管理器和 JDBC 驱动支持保存点。无外层事务时,两者都会新建事务。
11.3 捕获内层 REQUIRED 的运行时异常后,外层为什么仍提交失败?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释内外层为何共享回滚标记,以及捕获异常为何无法改变已经确定的回滚结果。
相关内容:REQUIRED。
参考回答
内层事务拦截器已将共享事务标记为 rollback-only。
外层捕获异常只改变控制流,回滚标记仍然保留。外层请求提交时,管理器回滚并抛出 UnexpectedRollbackException。
11.4 AFTER_COMMIT 监听器中继续写库,需要什么事务边界?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:说明提交后为何需要独立写入事务,并区分进程内通知与可恢复的持久化事件。
相关内容:提交后事件。
参考回答
提交后监听器的写入应通过另一个代理方法开启 REQUIRES_NEW。
监听器执行时原事务已提交,即使连接资源还可访问,继续使用它也不会再触发提交。因此,新写入需要自己的事务边界。
普通事务事件只在进程内保存,提交后崩溃会丢失未执行的通知。要求故障后仍能继续处理时,应在原事务中持久化 Outbox 事件,再由后台发布器重试投递。
12. 参考资料
- Spring Framework:Declarative Transaction Management
- Spring Framework:Transaction Abstraction
- Spring Framework:Transaction Propagation
- Spring Framework:Rolling Back a Declarative Transaction
- Spring API:
@Transactional - Spring Framework:Transaction-bound Events
- Spring Framework 6.2:DataSourceTransactionManager
- Spring Framework 6.2:DataSourceUtils
- Spring Framework 6.2:TransactionalEventListener