Spring 事务传播机制与 REQUIRES_NEW
一、核心概念
什么是事务传播行为
当一个事务方法调用另一个事务方法时,Spring 需要决定如何处理事务边界------是加入已有事务、新建独立事务,还是以无事务方式执行。这就是事务传播行为(Propagation Behavior)。
方法 A(有事务) → 调用方法 B(也声明了事务)
↓
B 是加入 A 的事务?
还是新建自己的事务?
还是挂起 A 的事务?
为什么需要 REQUIRES_NEW
默认传播行为 REQUIRED 表示"有事务就加入,没有就新建"。但在某些场景下,方法 B 的成功/失败不应影响方法 A ,或者方法 B 需要独立提交 (即使 A 后续回滚),此时需要 REQUIRES_NEW。
REQUIRED(默认): REQUIRES_NEW:
┌── 事务 A ──────────┐ ┌── 事务 A ──────────┐
│ 操作1 │ │ 操作1 │
│ ┌── 方法 B ────┐ │ │ ┌── 事务 B ────┐ │ ← 独立事务
│ │ 操作2 │ │ │ │ 操作2 │ │
│ │ 操作3 │ │ │ │ 操作3 │ │
│ └──────────────┘ │ │ └── 提交/回滚 ─┘ │
│ 操作4 │ │ 操作4 │
└── 全部提交或回滚 ───┘ └── 提交/回滚 ────────┘
↑ 两个事务互相独立
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、Spring 全部传播行为
| 传播行为 | 含义 | 适用场景 |
|---|---|---|
REQUIRED |
有事务加入,没有新建 | 默认选择,绝大多数业务方法 |
REQUIRES_NEW |
挂起当前事务,新建独立事务 | 独立提交、错误日志记录、MQ 消费 |
NESTED |
在当前事务中创建保存点(嵌套事务) | 部分失败可回滚到保存点 |
SUPPORTS |
有事务加入,没有就非事务执行 | 只读查询 |
NOT_SUPPORTED |
挂起当前事务,非事务执行 | 长时间查询不占事务连接 |
MANDATORY |
必须在已有事务中调用,否则抛异常 | 强制要求调用方提供事务 |
NEVER |
必须在无事务中调用,否则抛异常 | 明确不允许事务的操作 |
三、REQUIRES_NEW 的行为详解
执行流程
1. 调用方存在事务 T1
2. Spring 检测到 REQUIRES_NEW
3. 挂起事务 T1(释放 T1 的数据库连接回连接池?不,保持持有但暂停)
4. 新建事务 T2,获取新的数据库连接
5. 执行方法体
6. T2 提交或回滚
7. 恢复事务 T1,继续执行
隔离性
java
@Transactional
public void methodA() {
// 事务 T1
orderRepo.save(order); // T1 中写入数据
methodB(); // T2 独立提交
// 此时 T1 仍未提交
// T2 已提交的数据对 T1 可见(取决于隔离级别)
// 如果 T1 后续回滚,T2 的数据不受影响
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 事务 T2:独立于 T1
logRepo.save(errorLog); // T2 提交后立即持久化
// 即使 methodA 后续抛异常回滚 T1,这条日志也不会丢
}
关键特性
| 特性 | 说明 |
|---|---|
| 独立提交 | T2 提交不依赖 T1 的最终结果 |
| 独立回滚 | T2 回滚不影响 T1(除非异常被传播) |
| 新连接 | T2 使用独立的数据库连接 |
| 对 T1 不可见(默认) | T1 未提交的数据在 T2 中不可见(READ_COMMITTED 隔离级别下) |
| 死锁风险 | T1 锁的行如果 T2 也要锁,会死锁(T1 等 T2 完成,T2 等 T1 释放锁) |
四、典型应用场景
场景 1:MQ 消费方法独立事务
java
/**
* MQ 消费者调用 Service 层的处理方法.
* 为什么需要 REQUIRES_NEW:
* 1. MQ 框架调用消费方法时可能没有事务上下文
* 2. 需要明确的事务边界控制提交/回滚
* 3. 异常时只回滚本次业务操作,不影响消费框架
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
// 独立事务:成功则提交,失败则回滚
TaskLog taskLog = taskLogRepo.findById(logId).orElseThrow();
businessService.doWork(taskLog);
taskLog.setStatus("Y");
taskLogRepo.saveAndFlush(taskLog);
}
场景 2:错误日志独立保存
java
@Transactional(rollbackFor = Exception.class)
public void processOrder(Long orderId) {
try {
doBusinessLogic(orderId);
} catch (Exception e) {
// 业务失败,当前事务将回滚
// 但错误日志必须持久化(不能跟着回滚)
errorLogService.saveErrorLog(orderId, e.getMessage());
throw e;
}
}
// 错误日志服务
@Service
public class ErrorLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveErrorLog(Long bizId, String errorMsg) {
// 独立事务:即使外层事务回滚,错误日志也能保存
ErrorLog log = new ErrorLog();
log.setBizId(bizId);
log.setErrorMsg(errorMsg);
log.setCreateTime(new Date());
errorLogRepo.save(log);
}
}
场景 3:部分操作需要立即可见
java
@Transactional(rollbackFor = Exception.class)
public void createOrderAndNotify(OrderDto dto) {
// 主事务:创建订单(尚未提交)
Order order = createOrder(dto);
// 需要立即生成一个编号并让其他服务可查到
String seqNo = sequenceService.generateAndPersist(order.getId());
// 继续主流程...
order.setSeqNo(seqNo);
orderRepo.save(order);
}
@Service
public class SequenceService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String generateAndPersist(Long orderId) {
// 独立事务:立即提交,其他事务可以查到这个编号
String seq = generateNextSeq();
SeqRecord record = new SeqRecord(orderId, seq);
seqRepo.saveAndFlush(record);
return seq;
}
}
场景 4:批量处理中的单条隔离
java
@Service
public class BatchProcessor {
@Resource
private SingleItemProcessor singleItemProcessor;
/**
* 批量处理:每条记录独立事务.
* 一条失败不影响其他记录。
*/
public BatchResult processBatch(List<Long> itemIds) {
BatchResult result = new BatchResult();
for (Long itemId : itemIds) {
try {
singleItemProcessor.processOne(itemId);
result.addSuccess(itemId);
} catch (Exception e) {
// 单条失败不中断批量
result.addFailed(itemId, e.getMessage());
}
}
return result;
}
}
@Service
public class SingleItemProcessor {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processOne(Long itemId) {
// 每条记录一个独立事务
// 失败只回滚当前这条
Item item = itemRepo.findById(itemId).orElseThrow();
doProcess(item);
item.setStatus("DONE");
itemRepo.save(item);
}
}
五、注意事项与陷阱
5.1 自调用失效问题
java
@Service
public class OrderService {
// ❌ 错误:同类内部调用,事务注解不生效
@Transactional
public void methodA() {
this.methodB(); // 直接调用,不走代理,REQUIRES_NEW 失效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 实际仍在 methodA 的事务中执行
}
}
解决方案:
java
// 方案1:注入自身代理
@Service
public class OrderService {
@Resource
private OrderService self; // 注入代理对象
@Transactional
public void methodA() {
self.methodB(); // 通过代理调用,事务注解生效
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { ... }
}
// 方案2:拆分到不同 Service(推荐)
@Service
public class OrderService {
@Resource
private OrderLogService orderLogService;
@Transactional
public void methodA() {
orderLogService.methodB(); // 不同类,走代理
}
}
@Service
public class OrderLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { ... }
}
5.2 死锁风险
java
@Transactional
public void methodA() {
// T1 锁定了 order 表 id=1 的行
Order order = orderRepo.findByIdForUpdate(1L);
order.setStatus("PROCESSING");
orderRepo.save(order); // 行锁未释放(T1 未提交)
// 调用 REQUIRES_NEW 方法
auditService.audit(1L); // T2 开始
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(Long orderId) {
// T2 也要锁 order 表 id=1 的行
Order order = orderRepo.findByIdForUpdate(orderId); // 💀 死锁!
// T2 等待 T1 释放行锁
// T1 等待 T2 完成才能继续
}
规避方式:
java
// 方案1:REQUIRES_NEW 方法不锁相同行
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(Long orderId) {
// 只读查询,不加锁
Order order = orderRepo.findById(orderId).orElseThrow();
// 写入审计表(不同的表)
AuditLog audit = new AuditLog(orderId, "APPROVED");
auditLogRepo.save(audit);
}
// 方案2:在 REQUIRES_NEW 调用前释放行锁(flush + 不再修改)
@Transactional
public void methodA() {
Order order = orderRepo.findById(1L).orElseThrow();
order.setStatus("PROCESSING");
orderRepo.saveAndFlush(order); // flush 但不释放锁
// 改为事务提交后再调用审计(避免死锁)
// 或者审计方法操作不同的行/表
}
5.3 连接池耗尽
java
// ❌ 危险:嵌套过深的 REQUIRES_NEW
@Transactional
public void level1() {
level2(); // 新连接
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void level2() {
level3(); // 又一个新连接
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void level3() {
level4(); // 又一个新连接...
}
// 每层挂起的事务都持有一个连接
// 4 层嵌套 = 4 个数据库连接被同一个线程占用
// 并发量大时连接池迅速耗尽
规避方式:
- 控制 REQUIRES_NEW 嵌套深度(建议不超过 2 层)
- 尽量在最内层使用,不要级联嵌套
5.4 异常传播
java
@Transactional
public void methodA() {
try {
newTxService.methodB(); // REQUIRES_NEW,内部抛异常
} catch (Exception e) {
// T2 已回滚
// 这里 catch 住了,T1 不会回滚
log.warn("methodB 失败,继续执行 methodA", e);
}
// T1 继续正常提交
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// T2 内部抛异常 → T2 回滚
throw new RuntimeException("业务异常");
}
注意:如果 methodA 不 catch 异常,异常传播到 methodA 的事务边界时,T1 也会被标记为 rollback-only。
六、REQUIRES_NEW vs NESTED
| 维度 | REQUIRES_NEW | NESTED |
|---|---|---|
| 事务关系 | 完全独立的新事务 | 外层事务的子事务(保存点) |
| 数据库连接 | 使用新连接 | 共用外层连接 |
| 外层回滚影响 | 不影响内层(已提交) | 内层也回滚(保存点失效) |
| 内层回滚影响 | 不影响外层(catch 住) | 回滚到保存点,外层可继续 |
| 内层可见外层数据 | 不可见(不同连接) | 可见(同一连接) |
| JPA 支持 | 完全支持 | 取决于实现(不是所有 JPA 实现都支持) |
java
// NESTED:内层失败可回滚到保存点,外层继续
@Transactional
public void methodA() {
saveOrder(); // 保存订单
try {
nestedMethod(); // NESTED 事务
} catch (Exception e) {
// 内层回滚到保存点
// 外层 saveOrder() 的数据不受影响
}
saveLog(); // 继续执行
}
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
// 在保存点内执行
// 失败只回滚这部分
}
七、MQ 消费场景深入分析
为什么 MQ 消费方法适合 REQUIRES_NEW
java
// MQ 消费者框架代码(简化)
@Component
public class MqConsumer {
@RabbitListener(queues = "my-queue")
public void onMessage(Long logId) {
// 1. MQ 框架调用此方法时,通常没有外层事务
// 2. 即使有框架层事务,业务也应该隔离
try {
// REQUIRES_NEW 保证明确的事务边界
businessService.processInNewTx(logId);
// 成功 → T2 已提交 → ACK 消息
} catch (Exception e) {
// 失败 → T2 已回滚 → NACK/重试
log.warn("消费失败", e);
}
}
}
@Service
public class BusinessService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processInNewTx(Long logId) {
// 明确的独立事务:
// - 成功:数据提交,状态更新为 Y
// - 失败:数据回滚,状态保持 O/P
}
}
错误日志为什么也要 REQUIRES_NEW
java
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
try {
doBusinessLogic(logId);
markSuccess(logId);
} catch (Exception e) {
// 问题:如果在当前事务中写错误日志,事务回滚后日志也丢了
// 解决:错误日志写入方法也是 REQUIRES_NEW
errorLogService.saveError(logId, e); // 独立事务,不随本事务回滚
throw e; // 重新抛出,让本事务回滚
}
}
与事务后置动作的配合
java
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
// 业务逻辑...
doWork();
// 事务提交后才发送 MQ(保证数据可见性)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 此时 REQUIRES_NEW 的事务已提交
// 新的消费者能读到本次写入的数据
anotherMqSender.send(nextLogId);
}
});
}
八、完整示例:MQ 消费 + 独立事务 + 错误处理
java
/**
* MQ 消费者.
* 职责:消息接收、锁控制、异常处理
* 不含业务逻辑。
*/
@Component
@Slf4j
public class TaskMqConsumer {
@Resource
private TaskProcessService taskProcessService;
@Resource
private DistributedLockProvider lockProvider;
@RabbitListener(queues = "${mq.queue.task-process}")
public void consume(Long logId) {
log.info("收到消息, logId={}", logId);
String lockKey = "task:process:" + logId;
DistributedLock lock = lockProvider.getLock(lockKey, 60, TimeUnit.SECONDS);
if (!lock.tryLock(30, TimeUnit.SECONDS)) {
log.warn("获取锁失败, logId={}", logId);
return;
}
try {
// 调用独立事务的处理方法
taskProcessService.processInNewTransaction(logId);
} catch (Exception e) {
// 事务已回滚,消息可能重试
log.warn("任务处理失败, logId={}", logId, e);
} finally {
lock.unlock();
}
}
}
/**
* 任务处理 Service.
* REQUIRES_NEW 保证每次消费都是独立事务。
*/
@Service
@Slf4j
public class TaskProcessService {
@Resource
private TaskLogRepository taskLogRepository;
@Resource
private ErrorLogService errorLogService;
@Resource
private TaskProcessor taskProcessor;
@Resource
private FollowUpMqSender followUpMqSender;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processInNewTransaction(Long logId) {
// 1. 查询任务
TaskLog taskLog = taskLogRepository.findById(logId).orElse(null);
if (taskLog == null) {
log.warn("任务不存在, logId={}", logId);
return;
}
// 2. 幂等判断
if ("Y".equals(taskLog.getStatus())) {
return;
}
try {
// 3. 执行业务
taskProcessor.execute(taskLog);
// 4. 标记成功
taskLog.setStatus("Y");
taskLog.setErrorMsg(null);
taskLogRepository.saveAndFlush(taskLog);
// 5. 事务提交后触发后续动作
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
followUpMqSender.send(taskLog.getFollowUpId());
}
});
} catch (Exception e) {
log.warn("任务执行失败, logId={}", logId, e);
// 6. 错误日志独立保存(不受本事务回滚影响)
errorLogService.saveError(logId, e.getMessage());
throw e; // 本事务回滚
}
}
}
/**
* 错误日志服务.
* 独立事务保证错误信息不丢失。
*/
@Service
public class ErrorLogService {
@Resource
private TaskLogRepository taskLogRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveError(Long logId, String errorMsg) {
TaskLog taskLog = taskLogRepository.findById(logId).orElse(null);
if (taskLog == null) return;
taskLog.setStatus("P");
taskLog.setRetryCount(taskLog.getRetryCount() + 1);
taskLog.setErrorMsg(errorMsg != null ? errorMsg.substring(0, Math.min(errorMsg.length(), 500)) : null);
taskLogRepository.saveAndFlush(taskLog);
}
}
九、决策指南
什么时候用 REQUIRES_NEW
- ✅ MQ 消费者的核心处理方法
- ✅ 错误/审计日志写入(不能随业务事务回滚)
- ✅ 序列号/编号生成(需要立即提交避免重复)
- ✅ 批量操作中每条记录的独立处理
- ✅ 与外部系统交互前的状态锁定(提交后才调外部)
什么时候不该用 REQUIRES_NEW
- ❌ 普通的 Service 层方法调用(用默认 REQUIRED)
- ❌ 需要与调用方共享事务上下文的操作
- ❌ 深层嵌套调用(超过 2 层)
- ❌ 可能与外层事务锁相同数据行的操作(死锁)
- ❌ 纯查询方法(用 SUPPORTS 或 readOnly=true)