Spring 事务传播行为详解
事务传播行为(Propagation)定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。这是 Spring 事务属性中最核心、也最容易混淆的概念。
一、什么是传播行为
事务传播行为回答的问题是:方法 A 开启了事务,方法 B 也被 @Transactional 标注,当 A 调用 B 时,B 是加入 A 的事务,还是新建一个事务,还是以非事务方式执行?
Spring 定义了 7 种传播行为,定义在 Propagation 枚举中。
二、7 种传播行为总览
| 传播行为 | 含义 | 当前有事务 | 当前无事务 |
|---|---|---|---|
REQUIRED |
默认,加入或新建 | 加入当前事务 | 新建事务 |
REQUIRES_NEW |
总是新建独立事务 | 挂起当前,新建 | 新建事务 |
NESTED |
嵌套事务(保存点) | 创建保存点 | 新建事务 |
SUPPORTS |
支持当前事务 | 加入当前事务 | 非事务执行 |
NOT_SUPPORTED |
不支持事务 | 挂起当前,非事务执行 | 非事务执行 |
MANDATORY |
强制要求事务 | 加入当前事务 | 抛异常 |
NEVER |
禁止事务 | 抛异常 | 非事务执行 |
三、逐一详解
1. REQUIRED(默认)
语义:如果当前有事务,就加入;如果没有,就新建一个。
这是最常用的传播行为,也是默认值。绝大多数业务方法都用它。
java
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderDao.save(order);
// 调用另一个 REQUIRED 方法,加入当前事务
inventoryService.decrease(order.getProductId());
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRED)
public void decrease(Long productId) {
// 加入 OrderService 的事务
}
}
关键点 :createOrder 和 decrease 在同一个事务中。任何一方抛异常,整个事务回滚。
2. REQUIRES_NEW
语义:总是新建一个独立事务。如果当前有事务,挂起当前事务,新建一个,执行完后再恢复当前事务。
java
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderDao.save(order);
// 日志用独立事务,主业务回滚不影响日志
logService.saveLog("创建订单:" + order.getId());
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
logDao.save(message);
}
}
关键点:
saveLog在独立事务中执行,不受createOrder事务影响。- 即使
createOrder后续抛异常回滚,saveLog的记录已经提交,不会回滚。 - 两个事务使用不同的数据库连接。
典型场景:日志记录、审计操作、消息发送------这些操作不应该因为主业务失败而丢失。
3. NESTED
语义:如果当前有事务,在当前事务中创建一个保存点(Savepoint),嵌套事务执行。如果当前没有事务,则新建一个事务。
java
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderDao.save(order);
try {
// 嵌套事务,失败只回滚到保存点
inventoryService.decrease(order.getProductId());
} catch (Exception e) {
log.error("扣减库存失败,但订单保留", e);
}
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.NESTED)
public void decrease(Long productId) {
// 如果这里抛异常,只回滚到保存点
// 外部事务可以继续执行
}
}
关键点:
NESTED是外部事务的一部分,通过保存点实现部分回滚。- 内部事务失败,回滚到保存点,外部事务可以选择继续或回滚。
- 外部事务失败,内部事务也会一起回滚。
NESTED依赖数据库的保存点支持,MySQL InnoDB 支持。
典型场景:部分回滚,如批量处理中允许某些子操作失败而不影响整体。
4. SUPPORTS
语义:如果当前有事务,就加入;如果没有,就以非事务方式执行。
java
@Service
public class QueryService {
@Transactional(propagation = Propagation.SUPPORTS)
public User findById(Long id) {
return userDao.findById(id);
}
}
典型场景:查询方法,有事务就用事务,没有也能正常执行。
5. NOT_SUPPORTED
语义:以非事务方式执行。如果当前有事务,挂起当前事务。
java
@Service
public class NotifyService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(String message) {
// 非事务执行,不占用数据库连接
}
}
典型场景:不需要事务的操作,如发送消息、调用外部接口。
6. MANDATORY
语义 :必须在一个已有事务中执行,否则抛出 IllegalTransactionStateException。
java
@Service
public class InventoryService {
@Transactional(propagation = Propagation.MANDATORY)
public void decrease(Long productId) {
// 必须在事务中调用,否则报错
}
}
典型场景:强制要求调用方开启事务,确保数据一致性。
7. NEVER
语义:必须以非事务方式执行,如果当前有事务,抛出异常。
java
@Service
public class AuditService {
@Transactional(propagation = Propagation.NEVER)
public void audit(String message) {
// 不允许在事务中调用
}
}
典型场景:明确禁止事务的操作。
四、核心对比:REQUIRED vs REQUIRES_NEW vs NESTED
| 对比维度 | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| 事务数量 | 1 个 | 2 个独立事务 | 1 个事务 + 保存点 |
| 数据库连接 | 共用 | 各自独立 | 共用 |
| 内部回滚 | 整体回滚 | 不影响外部 | 回滚到保存点 |
| 外部回滚 | 内部一起回滚 | 不影响内部 | 内部一起回滚 |
| 依赖保存点 | 否 | 否 | 是 |
| 适用场景 | 大多数业务 | 日志、审计 | 部分回滚 |
用一张图说明:
REQUIRED:
[============ 同一个事务 ============]
外部方法 内部方法
任一失败 → 整体回滚
REQUIRES_NEW:
[==== 外部事务 ====]
[==== 内部事务 ====]
外部失败 → 内部已提交,不回滚
内部失败 → 外部可继续
NESTED:
[============ 外部事务 ============]
[== 保存点 ==]
内部失败 → 回滚到保存点,外部可继续
外部失败 → 全部回滚
五、实际案例
案例:下单 + 扣库存 + 记录日志
java
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private InventoryService inventoryService;
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
// 1. 保存订单(主事务)
orderDao.save(order);
// 2. 扣减库存(加入主事务)
inventoryService.decrease(order.getProductId(), order.getQuantity());
// 3. 记录日志(独立事务,主事务回滚日志也保留)
logService.saveLog("订单创建:" + order.getId());
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRED)
public void decrease(Long productId, int quantity) {
// 加入主事务,失败则整个订单回滚
inventoryDao.decrease(productId, quantity);
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
// 独立事务,不受主事务回滚影响
logDao.save(message);
}
}
执行结果:
- 订单保存成功、库存扣减成功 → 全部提交。
- 库存扣减失败 → 订单回滚,但日志已提交(独立事务)。
- 订单保存失败 → 库存和订单都回滚,日志保留。
六、常见坑
1. 内部调用传播行为失效
同一个类中方法 A 调用方法 B,B 的传播行为不生效。因为内部调用不经过代理。
java
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
this.saveLog(); // ❌ 传播行为不生效
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
// 不会新建独立事务
}
}
解决方案:注入自身代理,或把方法移到另一个 Bean 中。
2. REQUIRES_NEW 占用额外连接
REQUIRES_NEW 会挂起当前事务,新建一个数据库连接。如果并发量高,可能耗尽连接池。使用时要评估连接池大小。
3. NESTED 依赖数据库支持
NESTED 依赖数据库的保存点(Savepoint)功能。MySQL InnoDB 支持,但某些数据库或驱动可能不支持。使用前确认数据库支持。
4. 异常被吞掉导致传播行为不符预期
java
@Transactional(propagation = Propagation.REQUIRED)
public void outer() {
try {
innerService.doSomething(); // REQUIRES_NEW
} catch (Exception e) {
// 异常被吞掉,外部事务不会回滚
}
}
如果内部是 REQUIRES_NEW 且异常被吞掉,内部事务已回滚,外部事务继续执行。这可能是期望的行为,也可能不是,取决于业务需求。
七、总结
| 传播行为 | 一句话说明 | 常用度 |
|---|---|---|
REQUIRED |
加入或新建,默认 | ⭐⭐⭐⭐⭐ |
REQUIRES_NEW |
总是新建独立事务 | ⭐⭐⭐⭐ |
NESTED |
嵌套事务,支持部分回滚 | ⭐⭐⭐ |
SUPPORTS |
有则加入,无则非事务 | ⭐⭐⭐ |
NOT_SUPPORTED |
非事务执行,挂起当前 | ⭐⭐ |
MANDATORY |
必须在事务中,否则报错 | ⭐ |
NEVER |
禁止事务,否则报错 | ⭐ |
事务传播行为决定了多个事务方法调用时的事务边界。REQUIRED 是默认值,适合大多数业务场景。REQUIRES_NEW 用于需要独立提交的操作,如日志记录。NESTED 用于需要部分回滚的场景。理解它们的区别,才能在设计事务时做出正确的选择。