文章目录
-
- 打比方理解:餐厅结账
- 核心区别
- 代码示例
- 一个非常容易踩的坑
- [`REQUIRES_NEW` 也不是内层失败后外层一定成功](#
REQUIRES_NEW也不是内层失败后外层一定成功) - 什么时候使用哪个?
- [`REQUIRES_NEW` 的三个风险](#
REQUIRES_NEW的三个风险)
先纠正名称:Spring 里叫 REQUIRES_NEW ,没有 REQUIRED_NEW。
用一句话记忆:
REQUIRED:有事务就加入,没有就创建。
REQUIRES_NEW:不管有没有,都另开一个新事务。
打比方理解:餐厅结账
把"事务"想象成餐厅的一张账单。
REQUIRED:有账单就记在同一张上
外层方法已经开了一张账单,内层方法选择 REQUIRED:
"不用开新单,把我的消费也记在外层那张账单上。"
因此外层和内层实际上共用同一个数据库事务:
text
外层事务
├── 创建订单
├── 扣减库存(内层 REQUIRED)
└── 创建支付记录
任何一步失败,整张账单作废:
text
订单回滚
库存回滚
支付记录回滚
如果当前没有事务,REQUIRED 就自己创建一个。
REQUIRES_NEW:暂停旧账单,另开一张新账单
外层已经有事务,调用 REQUIRES_NEW:
"先把外层账单放一边,我单独开张账单,单独结账。"
text
外层事务A:创建订单
↓ 暂停
内层事务B:保存操作日志
↓ 独立提交
恢复外层事务A
↓
外层事务A失败并回滚
最终结果:
text
订单:回滚
日志:已经独立提交,仍然存在
Spring 官方定义也是如此:REQUIRED 加入现有事务;REQUIRES_NEW 暂停现有事务并创建独立物理事务。Spring 事务传播文档
核心区别
| 情况 | REQUIRED |
REQUIRES_NEW |
|---|---|---|
| 外层没有事务 | 创建新事务 | 创建新事务 |
| 外层已有事务 | 加入外层事务 | 暂停外层,另开事务 |
| 与外层是否同一事务 | 是 | 否 |
| 内层提交 | 等外层一起提交 | 内层立即独立提交 |
| 外层后来回滚 | 内层一起回滚 | 已提交的内层不回滚 |
| 数据库连接 | 通常共用外层连接 | 通常需要额外连接 |
| 常见用途 | 普通业务流程 | 日志、状态记录等独立操作 |
代码示例
REQUIRED:订单和库存必须同生共死
java
@Service
public class OrderService {
@Transactional
public void createOrder() {
orderMapper.insertOrder();
// InventoryService中的方法也是REQUIRED
inventoryService.deductStock();
throw new RuntimeException("创建支付记录失败");
}
}
java
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRED)
public void deductStock() {
inventoryMapper.deductStock();
}
}
因为 deductStock() 加入外层事务,所以最终:
text
订单插入:回滚
库存扣减:回滚
实际上 @Transactional 默认传播行为就是 REQUIRED,通常不写也可以:
java
@Transactional
等价于:
java
@Transactional(propagation = Propagation.REQUIRED)
REQUIRES_NEW:日志必须独立保存
java
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
auditLogMapper.insert(message);
}
}
java
@Service
public class OrderService {
@Transactional
public void createOrder() {
orderMapper.insertOrder();
auditLogService.saveLog("开始创建订单");
throw new RuntimeException("订单创建失败");
}
}
结果是:
text
订单:回滚
日志:保留
因为日志事务已经独立提交。
一个非常容易踩的坑
假设内层使用 REQUIRED,发生异常后,外层把异常吃掉了:
java
@Transactional
public void outer() {
try {
inventoryService.deductStock();
} catch (Exception e) {
// 吃掉异常
}
orderMapper.insertOrder();
}
你可能以为外层还能提交,但内层和外层共用事务,内层可能已经把整个事务标记为:
text
rollback-only:只能回滚
外层最后提交时,Spring 可能抛出:
text
UnexpectedRollbackException
就像账单已经被盖上"作废"章,即使经理说"我处理了",结账系统仍不允许提交。
REQUIRES_NEW 也不是内层失败后外层一定成功
如果内层异常继续向外抛:
java
@Transactional
public void outer() {
auditLogService.saveLog(); // REQUIRES_NEW,内部抛异常
orderMapper.insertOrder();
}
虽然两个事务相互独立,但异常传到外层后,外层也可能因为这个异常回滚。
如果业务允许内层失败、外层继续,外层必须明确处理异常:
java
@Transactional
public void outer() {
try {
auditLogService.saveLog();
} catch (Exception e) {
log.error("保存日志失败", e);
}
orderMapper.insertOrder();
}
区别是:REQUIRES_NEW 的内层回滚不会直接把外层标记为 rollback-only;但传播出去的异常仍可能触发外层自己的回滚规则。
什么时候使用哪个?
绝大多数业务使用 REQUIRED:
- 创建订单和订单明细
- 扣库存和记录库存流水
- 转账的扣款与入账
- 删除主表和关联数据
需要所有步骤"同生共死"。
少数真正独立的操作使用 REQUIRES_NEW:
- 无论主业务成功失败,都要记录审计日志
- 独立记录任务执行状态
- 保存失败原因或重试记录
- 某个子任务允许单独提交
REQUIRES_NEW 的三个风险
- 占用额外数据库连接
外层事务的连接不会释放,内层还要再申请一个连接。高并发时可能耗尽连接池,甚至造成等待或死锁。Spring 官方特别提醒
- 不要更新外层已经锁住的数据
外层事务锁住一行后,内层新事务又修改同一行,内层可能一直等待外层释放锁;但外层又在等待内层结束,最终超时。
- 同类内部调用可能不生效
这样调用:
java
this.saveLog();
可能绕过 Spring 代理,导致 REQUIRES_NEW 没有真正创建新事务。通常应拆到另一个 Spring Bean:
java
orderService
→ auditLogService.saveLog()
最终记忆口诀:
REQUIRED:加入团队,共同成败。
REQUIRES_NEW:另立项目,独立结算。日常默认选
REQUIRED,只有业务明确要求独立提交时才选REQUIRES_NEW。