事务传播行为(7种)
候选人小郑在面试字节 P6 时,面试官问道:
"Spring 事务的传播行为有哪几种?"
小郑说:"有 REQUIRED、REQUIRES_NEW...还有几个我记不清了..."
面试官追问:"REQUIRED 和 REQUIRES_NEW 的区别是什么?NESTED 呢?"
小郑说:"NESTED 是嵌套事务,REQUIRED 是加入现有事务..."
面试官:"那你说说它们分别适合什么场景?"
小郑答不上来。
【面试官心理】
这道题我用来测试候选人对 Spring 事务传播行为的深度理解。7 种传播行为中,REQUIRED、REQUIRES_NEW、NESTED 是最常用的。能说清区别的占 40%,能说清适用场景的占 20%。能说出 NESTED 的 Savepoint 机制的,基本都有实战经验。
一、核心问题 🔴
1.1 问题拆解
第一层:概念
- "Spring 事务有哪 7 种传播行为?"
- "什么是事务传播行为?为什么需要它?"
第二层:区别
- "REQUIRED 和 REQUIRES_NEW 的区别是什么?"
- "NESTED 和 REQUIRED 的区别是什么?"
- "SUPPORTS 和 NOT_SUPPORTED 适合什么场景?"
第三层:原理
- "Spring 是怎么实现这些传播行为的?"
- "REQUIRES_NEW 为什么会挂起当前事务?"
- "NESTED 的 Savepoint 是怎么工作的?"
第四层:实战
- "什么场景下应该用 REQUIRES_NEW 而不是 REQUIRED?"
- "NESTED 有什么限制?"
- "嵌套事务的回滚会有什么影响?"
1.2 ❌ 错误示范
候选人原话 A:"传播行为就是事务之间的调用规则,没什么特别的。"
问题诊断:
- 知道这个概念,但不理解为什么需要
- 说不清实际业务中的价值
候选人原话 B:"REQUIRES_NEW 就是每次都创建新事务,很简单。"
问题诊断:
- 知道结论,但不理解底层实现
- 不知道"挂起当前事务"是什么意思
- 不知道 NESTED 和 REQUIRES_NEW 的本质区别
候选人原话 C:"NESTED 和 REQUIRES_NEW 一样,都是新事务。"
问题诊断:
- 完全混淆了两个概念
- NESTED 是嵌套事务(同一连接 + Savepoint),不是新事务
- 这是面试中的高频陷阱
1.3 标准回答
P5 回答:7 种传播行为
Spring 定义了 7 种事务传播行为,核心作用是控制事务方法被调用时,如何决定是否加入现有事务或创建新事务:
public enum Propagation {
REQUIRED ("支持当前事务,如果没有则创建新事务"), // 默认
REQUIRES_NEW("创建新事务,挂起当前事务"),
SUPPORTS ("支持当前事务,如果没有则以非事务执行"),
NOT_SUPPORTED("不支持当前事务,挂起当前事务"),
MANDATORY ("必须在事务中执行,否则抛异常"),
NEVER ("不能在事务中执行,否则抛异常"),
NESTED ("在嵌套事务中执行,如果没有则创建新事务")
}
1.4 追问升级
追问 1:REQUIRED vs REQUIRES_NEW
这是最常用的对比,必须掌握:
// REQUIRED 场景:业务数据一致性优先
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // 订单表
inventoryService.deduct(order); // 库存表
paymentService.process(order); // 支付
// 要么全部成功,要么全部回滚
}
// REQUIRES_NEW 场景:日志记录不影响主业务
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// 日志需要单独提交,即使后续失败也不影响日志
try {
logService.info("订单创建成功: " + order.getId());
} catch (Exception e) {
// 吞掉异常,不影响主流程
}
throw new RuntimeException("支付失败"); // 这个异常会导致上面的 orderRepository.save() 回滚
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void info(String message) {
// 这个方法有自己的事务,独立提交
// createOrder 的异常不会导致这个事务回滚
}
REQUIRED 执行流程:
createOrder() 开启事务 T1
└─ orderRepository.save() 加入 T1
└─ logService.info() 加入 T1(同一个事务)
└─ 抛异常
└─ 整个事务 T1 回滚
└─ 订单和日志都回滚 ❌
REQUIRES_NEW 执行流程:
createOrder() 开启事务 T1
└─ orderRepository.save() 加入 T1
└─ logService.info() 挂起 T1,开启新事务 T2
└─ 正常执行
└─ T2 提交 ✓
└─ 抛异常
└─ T1 回滚
└─ 订单回滚 ❌
└─ 日志已提交,不受影响 ✓
追问 2:REQUIRES_NEW 的挂起机制
这是理解 REQUIRES_NEW 的关键:
// AbstractPlatformTransactionManager.getTransaction()
public final TransactionStatus getTransaction(TransactionDefinition definition) {
Object transaction = doGetTransaction();
// 检查是否已存在事务
if (isExistingTransaction(transaction)) {
// 已有事务 → 检查传播行为
return handleExistingTransaction(transaction, definition);
}
// 没有事务 → 根据传播行为处理
if (definition.getPropagationBehavior() == Propagation.REQUIRES_NEW) {
// 【关键】挂起当前事务,创建新事务
SuspendedResourcesHolder suspendedResources = suspend(null);
try {
Object newTransaction = doGetTransaction();
// 创建新的数据库连接
doBegin(newTransaction, definition);
return new TransactionStatus(...);
} finally {
// ...
}
}
}
private SuspendedResourcesHolder suspend(Object transaction) {
if (this.suspendedResources != null) {
// 1. 暂停当前事务(数据库连接)
// 2. 保存挂起状态到 SuspendedResourcesHolder
// 3. 释放当前连接
}
return suspendedResources;
}
"挂起"本质上是:
- 保存当前事务的上下文(Connection、状态等)
- 释放当前的数据库连接
- 获取新的数据库连接用于新事务
- 新事务完成后,恢复挂起的事务
追问 3:NESTED 嵌套事务
NESTED 是最容易被误解的传播行为:
// REQUIRED:异常导致全部回滚
@Transactional
public void methodA() {
doStep1(); // 成功
doStep2(); // 成功
doStep3(); // 抛异常
// 1、2、3 全部回滚
}
// NESTED:嵌套部分可以独立回滚
@Transactional
public void methodA() {
doStep1(); // 成功
doStep2(); // 成功
try {
methodB(); // NESTED
} catch (Exception e) {
// B 回滚,但 A 可以继续
// 1、2 仍然提交
}
doStep4(); // 继续执行
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
// 在嵌套事务中执行
// 可以回滚到 Savepoint,而不影响外层事务
}
NESTED 的 Savepoint 机制:
// JDBC 实现(伪代码)
Connection conn = getConnection();
conn.setAutoCommit(false);
// 创建 Savepoint
Savepoint savepoint = conn.setSavepoint("methodB_start");
try {
methodBLogic();
conn.releaseSavepoint(savepoint);
} catch (Exception e) {
// 回滚到 Savepoint
conn.rollback(savepoint);
// 继续执行 methodA 的后续逻辑
}
⚠️
NESTED 依赖于数据库的 Savepoint 机制。如果数据库不支持 Savepoint(如某些配置下的 MySQL),NESTED 会退化到 REQUIRED。Oracle 和 SQL Server 完全支持 Savepoint。
追问 4:其他传播行为
// SUPPORTS:支持但不强制
@Transactional(propagation = Propagation.SUPPORTS)
public void query() {
// 如果有外层事务,加入
// 如果没有外层事务,以非事务方式执行
// 适合:查询方法(不需要事务,但调用者可能有)
}
// NOT_SUPPORTED:挂起当前事务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void batchImport() {
// 挂起外层事务,以非事务方式执行
// 适合:大数据量导入(事务会影响性能)
}
// MANDATORY:强制在事务中
@Transactional(propagation = Propagation.MANDATORY)
public void mustInTransaction() {
// 如果没有外层事务,抛 IllegalStateException
// 适合:某些核心方法必须由调用者在事务中调用
}
// NEVER:强制不在事务中
@Transactional(propagation = Propagation.NEVER)
public void mustNotInTransaction() {
// 如果有外层事务,抛 IllegalStateException
// 适合:只读方法、某些不允许在事务中执行的场景
}
二、延伸问题 🟡
2.1 传播行为的常见坑
坑1:REQUIRES_NEW 和事务同步问题
@Transactional
public void methodA() {
// 在 REQUIRES_NEW 之前修改的数据
dataRepository.update(1); // 修改了数据
// REQUIRES_NEW 方法可以看到这个修改吗?
// 取决于数据库隔离级别:
// - READ_COMMITTED: 可以看到
// - READ_UNCOMMITTED: 可以看到
// - SERIALIZABLE: 取决于具体实现
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 这里可以看到 methodA 对数据库的修改吗?
}
坑2:NESTED 回滚后的继续执行
@Transactional
public void methodA() {
doStep1();
try {
methodB(); // NESTED
} catch (Exception e) {
// B 回滚到 Savepoint
// A 可以继续执行
log.warn("B 回滚了,但 A 继续");
}
doStep3(); // 继续执行
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
// NESTED 的 Savepoint 回滚后
// B 后续的逻辑还可以继续执行吗?
// —— 可以!但 B 对数据库的修改已经回滚了
}
2.2 默认传播行为
// Spring 的默认传播行为是 REQUIRED
// 但要注意:有些 ORM 框架可能有不同的默认行为
// 设置默认传播行为
@Transactional(propagation = Propagation.REQUIRED) // 这是默认值,可以不写
// 也可以在事务管理器级别配置
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDefaultTimeout(30);
tm.setValidateExistingTransaction(true);
return tm;
}
三、生产避坑
3.1 误用 REQUIRES_NEW 导致连接耗尽
// ❌ 错误:在循环中调用 REQUIRES_NEW 方法
@Transactional
public void processItems(List<Item> items) {
for (Item item : items) {
itemProcessor.process(item); // REQUIRES_NEW
// 每次调用都会创建一个新连接
// 如果 items 有 10000 个,数据库连接池可能被打满
}
}
// ✅ 正确做法
@Transactional
public void processItems(List<Item> items) {
for (Item item : items) {
try {
itemProcessor.process(item); // REQUIRES_NEW
} catch (Exception e) {
log.error("处理失败: " + item.getId(), e);
}
}
}
// 或者用批量处理代替循环调用
@Transactional
public void processItems(List<Item> items) {
itemProcessor.processBatch(items); // 一个 REQUIRES_NEW 调用处理批量
}
3.2 NESTED 和数据库支持
// 检查数据库是否支持 Savepoint
// MySQL InnoDB: 支持
// MySQL MyISAM: 不支持,NESTED 会退化为 REQUIRED
// Oracle: 支持
// PostgreSQL: 支持
// 检测代码
Connection conn = dataSource.getConnection();
DatabaseMetaData meta = conn.getMetaData();
boolean supportsSavepoints = meta.supportsSavepoints();
// 在不支持 Savepoint 时,Spring 会抛异常或降级
// 可以通过配置强制使用
@Transactional(propagation = Propagation.NESTED,
rollbackFor = Exception.class)
// 如果数据库不支持 Savepoint,会抛出
// NestedTransactionNotSupportedException
3.3 传播行为和异常处理的组合
// 最常见的坑:REQUIRES_NEW + 异常被吞
@Transactional
public void methodA() {
try {
methodB(); // REQUIRES_NEW
} catch (Exception e) {
// ❌ 吞掉异常
// methodB 的事务已经回滚了
// 但 methodA 认为 B 失败了却继续执行
}
doSomethingElse(); // 继续执行,可能导致数据不一致
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
throw new RuntimeException("失败");
// B 的事务回滚
// 但 A 吞掉了异常
}
四、工程选型
4.1 传播行为选择指南
4.2 实战配置示例
// 日志服务 - 需要独立提交
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(String action, String details) {
auditLogRepository.save(new AuditLog(action, details));
}
}
// 消息发送服务 - 事务后发送
@Service
public class MessageService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendAfterCommit(Message message, Long transactionId) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
messageBroker.send(message);
}
}
);
// 保存消息记录
messageRepository.save(message);
}
}
// 查询服务 - 不强制事务
@Service
public class QueryService {
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
五、面试总结
事务传播行为是 Spring 事务中最考察理解的知识点。
P5 候选人能说出 7 种传播行为的名称。
P6 候选人能说清 REQUIRED、REQUIRES_NEW、NESTED 的区别,能解释"挂起"和"Savepoint"的概念。
P7 候选人能说出每种传播行为的适用场景和限制,能解释为什么 NESTED 可能退化,能分析传播行为和异常处理的组合问题。
记住,传播行为不是背名字,而是理解"事务边界在哪里、回滚影响范围是什么"。