Spring 事务传播行为七种解析
候选人小陈在面试拼多多时,被问到"Spring 事务的七种传播行为"。
小陈一口气背了出来:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
面试官点点头:"好,那你告诉我,NESTED 和 REQUIRES_NEW 的区别是什么?"
小陈说:"NESTED 是嵌套事务,REQUIRES_NEW 是开启新事务。"
面试官追问:"具体怎么实现的?"
小陈支支吾吾答不上来。面试官继续:"如果外层方法没有 @Transactional,内层方法用 NESTED,会怎样?"
小陈彻底卡住。
【面试官心理】
七种传播行为,90% 的候选人能背出名字,50% 能说出一两个典型场景,但能讲清楚 NESTED 的 Savepoint 机制、以及它和 REQUIRES_NEW 的本质区别的,只有 10%。这道题是 P6 和 P7 的天然分水岭。
一、七种传播行为一览表 🔴
二、REQUIRED:最常用的默认值 🔴
2.1 语义
如果当前存在事务,加入该事务;如果不存在事务,创建一个新事务。
2.2 典型场景
@Service
public class TransferService {
@Transactional
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// 外层开启事务
accountService.deduct(fromAccount, amount); // ✅ 加入外层事务
accountService.add(toAccount, amount); // ✅ 加入外层事务
notificationService.sendNotify(toAccount); // ✅ 加入外层事务
}
}
@Service
public class AccountService {
@Transactional(propagation = Propagation.REQUIRED)
public void deduct(String account, BigDecimal amount) {
// 复用外层事务,任何一步失败全部回滚
accountMapper.deduct(account, amount);
}
@Transactional(propagation = Propagation.REQUIRED)
public void add(String account, BigDecimal amount) {
accountMapper.add(account, amount);
}
}
三个方法共用同一个事务,任何一步失败,全部回滚。这是最常见的事务传播场景。
2.3 外层无事务时
@Transactional
public void outerMethod() {
// 外层开启事务
innerMethod(); // REQUIRES_NEW,创建新的事务
}
public void noTransactionMethod() {
// ⚠️ 外层无事务
innerMethod(); // REQUIRED,自动创建新事务
}
@Transactional(propagation = Propagation.REQUIRED)
public void innerMethod() {
// REQUIRED 保证它一定在事务中运行
}
三、REQUIRES_NEW:独立事务的守护者 🔴
3.1 语义
挂起当前事务(如果有),创建新事务。内外层事务完全隔离,互不影响。
3.2 典型场景
日志记录是最经典的 REQUIRES_NEW 场景——日志的提交不应受主业务事务回滚的影响。
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 1. 保存订单(主业务,失败要回滚)
orderMapper.insert(order);
// 2. 扣减库存
stockService.reduce(order.getSkuId(), order.getQuantity());
// 3. 发送通知(REQUIRES_NEW,独立事务)
// 即使主事务回滚,消息也要发出去
messageService.sendOrderCreated(order.getId());
}
}
@Service
public class MessageService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendOrderCreated(Long orderId) {
// 独立事务,不受 OrderService 的事务影响
// 如果主事务回滚,这条消息仍然会发送
messageMapper.insert(orderId, "ORDER_CREATED");
}
}
💡
这里有一个经典的面试陷阱:很多人以为"消息发送"用 REQUIRES_NEW 是为了"解耦性能",但真正的原因是数据一致性。如果消息发送和主业务在同一个事务里,当主事务回滚时消息也被回滚,消息队列就永远收不到这条消息。
3.3 外层无事务时
public void noTransactionOuter() {
// ⚠️ 外层无事务
reRequiresNewMethod(); // REQUIRES_NEW 会创建一个新的事务
}
即使外层没有事务,REQUIRES_NEW 也会创建新事务。这和 REQUIRED 不同——REQUIRED 在外层无事务时创建新事务,REQUIRES_NEW 在任何情况下都创建新事务。
四、NESTED:Savepoint 的艺术 🟡
4.1 语义
如果当前存在事务,在当前事务的嵌套级别创建一个 Savepoint(保存点);如果不存在事务,创建新事务。
这是七种传播行为中最难理解的一个,核心概念是数据库 Savepoint。
4.2 Savepoint 机制详解
数据库 Savepoint(保存点)允许将一个大事务拆分成多个子阶段,每个子阶段可以部分回滚——只回滚到某个保存点,而不是全部回滚。
@Transactional
public void batchImport(List<Record> records) {
int count = 0;
for (Record record : records) {
try {
importOne(record); // NESTED,每条记录一个保存点
count++;
} catch (Exception e) {
// ⚠️ 只回滚到当前保存点,不影响已成功的记录
log.warn("第{}条记录导入失败,继续处理", count + 1);
}
}
log.info("成功导入 {} 条记录", count);
}
@Transactional(propagation = Propagation.NESTED)
public void importOne(Record record) {
// 如果这里抛异常,只回滚这一条
// 不会影响 batchImport 中已经成功导入的其他记录
recordMapper.insert(record);
}
4.3 NESTED vs REQUIRES_NEW:核心区别
这是 P6/P7 最重要的区分问题:
// REQUIRES_NEW 的执行流程(伪代码)
@Transactional
public void outer() {
// 开启事务A
System.out.println("事务A开始");
inner_REQUIRES_NEW(); // ⚠️ 事务A被挂起
// 事务A被恢复(inner提交后才恢复)
System.out.println("事务A继续");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner_REQUIRES_NEW() {
// 开启事务B(与事务A完全独立)
System.out.println("事务B开始和提交");
// 事务B提交后,事务A恢复
}
// 输出:事务A开始 → 事务B开始和提交 → 事务A继续
// 如果 inner 抛异常回滚,事务A继续执行(事务B已提交,不受影响)
// NESTED 的执行流程(伪代码)
@Transactional
public void outer() {
// 开启事务A(也是事务B的事务,因为是嵌套)
System.out.println("事务开始,Savepoint创建");
inner_NESTED(); // 创建Savepoint
// ⚠️ inner回滚到Savepoint,但外层事务不全部回滚
System.out.println("外层继续执行");
}
@Transactional(propagation = Propagation.NESTED)
public void inner_NESTED() {
// 与外层同一物理事务,但有Savepoint隔离
System.out.println("嵌套事务执行");
// 如果这里抛异常,只回滚到Savepoint
}
// 输出:事务开始,Savepoint创建 → 嵌套事务执行
// 如果 inner 抛异常,外层收到异常,但如果外层捕获了
// 外层仍然可以继续执行(因为只回滚到Savepoint)
⚠️
NESTED 的关键限制:只支持 DataSourceTransactionManager(JDBC 单数据源事务)。如果使用 JtaTransactionManager(分布式事务),NESTED 会被降级为 REQUIRED。原因很简单:分布式事务不支持 Savepoint 机制。
五、SUPPORTS、MANDATORY:查询的哲学 🟡
5.1 SUPPORTS:跟着外层走
如果当前有事务,加入事务;如果没有,以非事务方式运行。
@Transactional(propagation = Propagation.SUPPORTS)
public User getUser(Long id) {
// 有外层事务时,加入事务(确保读一致性)
// 无外层事务时,普通查询(允许脏读等)
return userMapper.selectById(id);
}
5.2 MANDATORY:必须有事务
如果当前有事务,加入事务;如果没有,抛 IllegalStateException。
@Transactional(propagation = Propagation.MANDATORY)
public void updateUser(User user) {
// 确保调用者必须在事务中调用
// 如果在非事务方法中被调用,直接抛异常
userMapper.update(user);
}
// 使用方
@Transactional
public void serviceMethod() {
updateUser(user); // ✅ 正常执行
}
public void nonTransactionMethod() {
updateUser(user); // ⚠️ 抛 IllegalStateException
}
六、NOT_SUPPORTED 和 NEVER:明确拒绝事务 🟡
6.1 NOT_SUPPORTED:挂起事务
如果当前有事务,挂起它,以非事务方式运行。
@Transactional
public void importDataFromThirdParty() {
// 主业务在事务中
processMainData();
// 调用第三方接口,耗时很长
// ⚠️ 如果这个在事务中运行,会长时间占用数据库连接
// 导致其他请求超时
// 用 NOT_SUPPORTED 挂起事务,避免长时间占用连接
thirdPartyService.syncData(NOT_SUPPORTED);
}
6.2 NEVER:强制非事务
如果当前有事务,抛 RemoteException;如果没有,以非事务运行。
这是一个防御性的传播行为,用于确保方法绝对不在事务中运行——通常用于防止误用。
七、外层无事务时的边界情况 🔴
这是面试官最爱的追问方向,能答出来的候选人凤毛麟角:
关键发现:
- MANDATORY 是唯一一个在外层无事务时会抛异常的传播行为
- NESTED 在外层无事务时,会降级为创建新事务(等价于 REQUIRED)
八、❌ 错误示范
候选人原话一:
"NESTED 就是嵌套事务,和 REQUIRES_NEW 差不多,都是开启新事务。"
问题诊断:
- NESTED 和 REQUIRES_NEW 是完全不同的机制
- NESTED 是同一物理事务的嵌套级别,用 Savepoint 部分回滚
- REQUIRES_NEW 是完全独立的物理事务
- 混淆两者的候选人,90% 没有实际用过嵌套事务
候选人原话二:
"SUPPORTS 就是如果有事务就用,没有就不用,很简单。"
问题诊断:
- 只理解了字面意思,没理解"以非事务运行"意味着什么
- 非事务运行意味着不会开启数据库事务,脏读、不可重复读都有可能发生
- 在查询方法上用 SUPPORTS,需要开发者非常清楚隔离级别的影响
候选人原话三:
"NESTED 在 JTA 分布式事务下也能工作得很好。"
问题诊断:
- JTA 不支持 Savepoint,所以 NESTED 在 JTA 环境下会被降级为 REQUIRED
- 如果依赖 Savepoint 的部分回滚功能,在分布式环境下会完全失效
【面试官心理】
这道题我通常这样追问:NESTED 为什么在 JTA 环境下不工作?很多候选人说"因为 JTA 不支持 Savepoint",但说不出为什么 JTA 不支持,以及降级后的行为是什么。能完整回答的,基本都是 P7 候选人。
九、工程选型指南