事务传播行为定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring 提供了七种传播行为,理解它们的差异和适用场景,是设计可靠业务逻辑的前提。本节将逐一剖析每种行为,并给出真实业务中的选型建议。
10.2.1 传播行为概览
在 Spring 中,传播行为通过 @Transactional(propagation = Propagation.XXX) 指定,对应的枚举定义在 org.springframework.transaction.annotation.Propagation 中。七种行为按是否支持当前事务可分为两大类:
| 传播行为 | 说明 |
|---------|------|
| REQUIRED | 默认行为。当前有事务则加入,无则新建 |
| REQUIRES_NEW | 挂起当前事务,始终新建独立事务 |
| SUPPORTS | 当前有事务则加入,无则非事务执行 |
| NOT_SUPPORTED | 挂起当前事务,始终非事务执行 |
| MANDATORY | 必须在事务中执行,否则抛出异常 |
| NEVER | 必须非事务执行,否则抛出异常 |
| NESTED | 当前有事务则创建保存点作为嵌套子事务,无则新建 |
10.2.2 逐一详解与场景分析
1. REQUIRED:默认选择,最自然的传播方式
REQUIRED 是 @Transactional 的默认传播行为,也是实际项目中使用频率最高的一种。它的逻辑非常简单:当前存在事务,就加入这个事务;当前没有事务,就自己创建一个。
运行逻辑:
- 调用方如果已经开启了事务,被调用的方法会沿用同一个数据库连接,共享同一个事务上下文。
- 调用方如果没有事务(例如从一个普通 Controller 调用 Service),Spring 会为该方法新建一个事务。
典型场景:
几乎所有标准的 Service 层方法都适合用 REQUIRED。比如一个下单服务:
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
orderDao.insert(order);
inventoryService.reduceStock(order); // 这里也使用 REQUIRED
paymentService.charge(order); // 这里也使用 REQUIRED
}
}
在这个调用链中,reduceStock 和 charge 都加入到 placeOrder 开启的事务中,任何一个操作失败,整个订单创建过程都会回滚,保证了数据一致性。
注意事项:
- 如果加入当前事务,被调用方的回滚标记会影响整个事务。例如
charge中抛出RuntimeException,整个外层placeOrder也会回滚。这通常符合业务预期,但如果你不想子方法的失败干扰主流程,则需要考虑其他传播行为。 - 因为是同一个事务,所以持有相同的数据库连接和锁,要警惕长事务导致连接耗尽或锁竞争。
2. REQUIRES_NEW:独立事务,用于逻辑隔离
REQUIRES_NEW 在每次调用时都会新建一个事务,并将当前事务(如果存在)挂起。这意味着内部方法的事务与外层事务完全独立:提交和回滚互不影响。
运行逻辑:
- 外层存在事务 A,调用一个
REQUIRES_NEW的方法时,事务 A 会被挂起(数据库连接被暂存)。 - 内部方法开启全新的事务 B,使用新的数据库连接,提交或回滚都是独立的。
- 事务 B 完成后,外层事务 A 恢复继续执行。
典型场景:
- 日志审计:业务操作失败需要回滚,但操作日志必须保留。即使主业务回滚,日志记录也要成功提交。
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordAudit(String action) {
// 写入审计表,独立事务
auditDao.insert(action);
}
}
- 异步任务状态记录:在主流程中更新任务状态为“处理中”,这个状态不能被后续的业务回滚所清除。
注意事项:
- 因为使用了新的数据库连接,如果外层事务持有某些锁,内层
REQUIRES_NEW可能会等待外层锁释放而造成死锁,需要小心资源竞争顺序。 - 过度使用会造成连接数膨胀,因为同时可能存在多个挂起的事务持有连接。
3. NESTED:嵌套事务,保存点回滚
NESTED 是 JDBC 保存点(Savepoint)机制的一种封装。它允许在当前事务中创建一个嵌套的子事务,子事务可以独立回滚到保存点,而不影响外层事务的继续执行,但最终的提交仍然依赖于外层事务。
运行逻辑:
- 如果当前存在事务,就在当前事务中创建一个保存点,然后执行业务逻辑。
- 如果子事务失败,Spring 会回滚到保存点,即撤销子事务内部的所有操作,但外层事务不受影响。
- 最终的外层事务提交时,嵌套子事务已提交或已回滚的部分会一并决定最终状态。
与 REQUIRES_NEW 的关键区别:
NESTED复用外层事务的数据库连接,不需要额外连接。NESTED的提交是延迟的:子事务提交只是将保存点释放,真正的持久化要等到外层事务提交才生效。外层事务回滚会连子事务一起回滚。REQUIRES_NEW则完全独立,内层提交立刻持久化,外层回滚不影响内层。
典型场景:
- 批量处理中的部分失败容忍:处理一批订单,每一条处理作为一个嵌套事务,失败则回滚该条,不影响整体批次。
@Transactional
public void processBatch(List<Order> orders) {
for (Order order : orders) {
try {
orderProcessor.processSingle(order); // NESTED
} catch (Exception e) {
log.error("处理订单 {} 失败, 继续下一条", order.getId());
}
}
}
processSingle 使用 NESTED,如果某条失败,只回滚该条的数据变更,外层事务继续执行。
注意事项:
NESTED依赖 JDBC 驱动对保存点的支持,需确认当前数据库驱动和连接池是否兼容。- 仅在当前已有事务时才会创建嵌套子事务,如果当前没有事务,行为等同于
REQUIRED(新建事务)。
4. SUPPORTS:跟随调用方,可有可无
SUPPORTS 表示如果有事务就加入,没有就算了,非事务执行。它非常“佛系”,适合于那些不需要强制事务一致性的只读操作。
典型场景:
- 查询方法通常不需要事务,但调用方可能处于一个写事务中,此时
SUPPORTS能让查询复用同一个连接,看到当前事务未提交的数据。
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public User getUser(Long id) {
return userDao.findById(id);
}
- 对于某些仅做数据转换、无持久化操作的 Service 方法,
SUPPORTS也可以减少事务开销。
注意事项:
- 如果以非事务方式执行,方法内部每个操作可能使用自动提交,不能保证多个数据库操作的原子性。
5. NOT_SUPPORTED:强制非事务执行
NOT_SUPPORTED 表示挂起当前事务,以非事务方式执行。即使外层有事务,内层方法也会脱离事务上下文。
典型场景:
- 执行一些不需要事务、且不希望受外层事务回滚影响的耗时操作,比如调用外部 HTTP 接口、发送邮件、文件处理等。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(Message msg) {
// 发送邮件或短信,不应在业务事务内
mailSender.send(msg);
}
注意事项:
- 一旦脱离事务,内部操作就丧失了原子性,适合那些需要“尽力而为”的逻辑。
6. MANDATORY:强制必须在事务中
MANDATORY 表示当前必须在事务中执行,否则直接抛异常。它不会主动创建事务,只是做一个前置检查。
典型场景:
- 需要严格保证操作与其他数据库操作在同一个事务中的工具方法,比如更新缓存前必须确保与数据库写入在一个事务内,防止数据不一致。
@Transactional(propagation = Propagation.MANDATORY)
public void evictCache(Long userId) {
// 清除用户缓存,必须与数据库更新在同一事务
cacheManager.evict("user:" + userId);
}
如果有人在非事务上下文调用了这个方法,Spring 会抛出 IllegalTransactionStateException,在开发阶段就能暴露调用异常。
7. NEVER:强制必须非事务执行
NEVER 与 MANDATORY 相反,表示必须在非事务中执行,如果存在事务则直接抛异常。这通常用于明确声明某个方法绝对不允许在事务中执行。
典型场景:
- 某些资源密集型操作(例如生成大文件、调用不支持事务的外部系统),一旦放入事务可能会导致长事务锁定资源。
@Transactional(propagation = Propagation.NEVER)
public void exportReport(OutputStream out) {
// 报表导出不能有事务
reportGenerator.generate(out);
}
强制抛出异常可以防止被误用。
10.2.3 选型决策模型
在日常开发中,传播行为的选型可以按照以下逻辑进行判断:
- 默认使用 REQUIRED。90% 以上的 Service 方法都应该在事务中运行。如果调用链复杂,保持 REQUIRED 可以让所有操作共享同一事务,天然形成原子性。
- 如果需要逻辑上的独立提交(子操作的成功不依赖主操作,但主操作失败不应回滚子操作),选择 REQUIRES_NEW。常用于日志、审计、状态记录等旁路逻辑。
- 如果需要部分失败不影响整体,并且希望复用同一个数据库连接,选择 NESTED。注意 Savepoint 支持和批量处理场景下的适用性。
- 对于查询或非关键方法,使用 SUPPORTS 或 NOT_SUPPORTED。SUPPORTS 更温和,能融入事务也可以独立运行;NOT_SUPPORTED 强制挂起事务,适合跟外部系统交互。
- 对于需要强制约束的方法,使用 MANDATORY 或 NEVER。它们更像是一种防御性编程,在团队协作或公共库中明确调用约定。
一个多场景组合示例:
@Service
@Transactional // 类级别默认 REQUIRED
public class OrderProcessingService {
// 1. 主流程:REQUIRED,整体原子性
public void processOrder(Order order) {
orderDao.updateStatus(order, Status.PROCESSING); // 修改状态
auditService.log("开始处理"); // 独立事务
try {
executeBizLogic(order); // 子流程
} catch (Exception e) {
// 处理异常,记录失败但主流程状态不变
auditService.log("处理失败"); // 独立事务
}
}
// 2. 子流程:NESTED,失败回滚子事务,主事务继续
@Transactional(propagation = Propagation.NESTED)
public void executeBizLogic(Order order) {
inventoryService.deduct(order); // 扣库存
paymentService.charge(order); // 扣款
}
// 3. 审计日志:REQUIRES_NEW,独立提交
@Service
public static class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action) {
auditDao.insert(action);
}
}
}
10.2.4 常见误区与避坑指南
1. 默认 REQUIRED 并非银弹
在一个长事务中不加控制地使用 REQUIRED 传播,可能导致事务时间过长,数据库锁持有过久,引发连接池耗尽、死锁等性能问题。应在设计时尽量缩小事务边界,将非关键操作移出事务。
2. REQUIRES_NEW 引发连接饥饿
每次 REQUIRES_NEW 都会消耗一个新的数据库连接,而挂起的外层连接也不会释放。如果在一个循环中反复调用 REQUIRES_NEW 的方法,连接数将快速上升。此时应优先考虑 NESTED 或重新设计批量处理逻辑。
3. 自调用导致传播失效
Spring 事务传播是通过 AOP 代理实现的。如果在同一个类中,方法 A 调用方法 B(即使 B 标注了不同的传播行为),由于是 this 内部调用,代理不生效,B 的事务设置将被忽略。解决方案是将 B 提取到另一个 Spring Bean 中,或通过 AopContext.currentProxy() 获取当前代理对象调用。
4. NESTED 与数据库支持
NESTED 依赖 JDBC Savepoint,部分数据库或某些 JDBC 驱动的特定版本可能不完全支持。在使用前应确认技术栈兼容性,并在集成测试中验证嵌套回滚是否达到预期。
事务传播行为的选择,本质上是业务语义在数据一致性层面的映射。只有充分理解每种传播行为在数据库连接、事务状态上的实际效果,才能写出既可靠又高效的业务代码。在下一节中,我们将深入事务的隔离级别,解决并发访问下的数据一致性问题。