Spring 事务传播机制与 REQUIRES_NEW

📅 2026/7/30 20:46:39 👁️ 阅读次数 📝 编程学习
Spring 事务传播机制与 REQUIRES_NEW

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,继续执行

隔离性

@TransactionalpublicvoidmethodA(){// 事务 T1orderRepo.save(order);// T1 中写入数据methodB();// T2 独立提交// 此时 T1 仍未提交// T2 已提交的数据对 T1 可见(取决于隔离级别)// 如果 T1 后续回滚,T2 的数据不受影响}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidmethodB(){// 事务 T2:独立于 T1logRepo.save(errorLog);// T2 提交后立即持久化// 即使 methodA 后续抛异常回滚 T1,这条日志也不会丢}

关键特性

特性说明
独立提交T2 提交不依赖 T1 的最终结果
独立回滚T2 回滚不影响 T1(除非异常被传播)
新连接T2 使用独立的数据库连接
对 T1 不可见(默认)T1 未提交的数据在 T2 中不可见(READ_COMMITTED 隔离级别下)
死锁风险T1 锁的行如果 T2 也要锁,会死锁(T1 等 T2 完成,T2 等 T1 释放锁)

四、典型应用场景

场景 1:MQ 消费方法独立事务

/** * MQ 消费者调用 Service 层的处理方法. * 为什么需要 REQUIRES_NEW: * 1. MQ 框架调用消费方法时可能没有事务上下文 * 2. 需要明确的事务边界控制提交/回滚 * 3. 异常时只回滚本次业务操作,不影响消费框架 */@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessMessage(LonglogId){// 独立事务:成功则提交,失败则回滚TaskLogtaskLog=taskLogRepo.findById(logId).orElseThrow();businessService.doWork(taskLog);taskLog.setStatus("Y");taskLogRepo.saveAndFlush(taskLog);}

场景 2:错误日志独立保存

@Transactional(rollbackFor=Exception.class)publicvoidprocessOrder(LongorderId){try{doBusinessLogic(orderId);}catch(Exceptione){// 业务失败,当前事务将回滚// 但错误日志必须持久化(不能跟着回滚)errorLogService.saveErrorLog(orderId,e.getMessage());throwe;}}// 错误日志服务@ServicepublicclassErrorLogService{@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidsaveErrorLog(LongbizId,StringerrorMsg){// 独立事务:即使外层事务回滚,错误日志也能保存ErrorLoglog=newErrorLog();log.setBizId(bizId);log.setErrorMsg(errorMsg);log.setCreateTime(newDate());errorLogRepo.save(log);}}

场景 3:部分操作需要立即可见

@Transactional(rollbackFor=Exception.class)publicvoidcreateOrderAndNotify(OrderDtodto){// 主事务:创建订单(尚未提交)Orderorder=createOrder(dto);// 需要立即生成一个编号并让其他服务可查到StringseqNo=sequenceService.generateAndPersist(order.getId());// 继续主流程...order.setSeqNo(seqNo);orderRepo.save(order);}@ServicepublicclassSequenceService{@Transactional(propagation=Propagation.REQUIRES_NEW)publicStringgenerateAndPersist(LongorderId){// 独立事务:立即提交,其他事务可以查到这个编号Stringseq=generateNextSeq();SeqRecordrecord=newSeqRecord(orderId,seq);seqRepo.saveAndFlush(record);returnseq;}}

场景 4:批量处理中的单条隔离

@ServicepublicclassBatchProcessor{@ResourceprivateSingleItemProcessorsingleItemProcessor;/** * 批量处理:每条记录独立事务. * 一条失败不影响其他记录。 */publicBatchResultprocessBatch(List<Long>itemIds){BatchResultresult=newBatchResult();for(LongitemId:itemIds){try{singleItemProcessor.processOne(itemId);result.addSuccess(itemId);}catch(Exceptione){// 单条失败不中断批量result.addFailed(itemId,e.getMessage());}}returnresult;}}@ServicepublicclassSingleItemProcessor{@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessOne(LongitemId){// 每条记录一个独立事务// 失败只回滚当前这条Itemitem=itemRepo.findById(itemId).orElseThrow();doProcess(item);item.setStatus("DONE");itemRepo.save(item);}}

五、注意事项与陷阱

5.1 自调用失效问题

@ServicepublicclassOrderService{// ❌ 错误:同类内部调用,事务注解不生效@TransactionalpublicvoidmethodA(){this.methodB();// 直接调用,不走代理,REQUIRES_NEW 失效!}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidmethodB(){// 实际仍在 methodA 的事务中执行}}

解决方案:

// 方案1:注入自身代理@ServicepublicclassOrderService{@ResourceprivateOrderServiceself;// 注入代理对象@TransactionalpublicvoidmethodA(){self.methodB();// 通过代理调用,事务注解生效}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidmethodB(){...}}// 方案2:拆分到不同 Service(推荐)@ServicepublicclassOrderService{@ResourceprivateOrderLogServiceorderLogService;@TransactionalpublicvoidmethodA(){orderLogService.methodB();// 不同类,走代理}}@ServicepublicclassOrderLogService{@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidmethodB(){...}}

5.2 死锁风险

@TransactionalpublicvoidmethodA(){// T1 锁定了 order 表 id=1 的行Orderorder=orderRepo.findByIdForUpdate(1L);order.setStatus("PROCESSING");orderRepo.save(order);// 行锁未释放(T1 未提交)// 调用 REQUIRES_NEW 方法auditService.audit(1L);// T2 开始}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidaudit(LongorderId){// T2 也要锁 order 表 id=1 的行Orderorder=orderRepo.findByIdForUpdate(orderId);// 💀 死锁!// T2 等待 T1 释放行锁// T1 等待 T2 完成才能继续}

规避方式:

// 方案1:REQUIRES_NEW 方法不锁相同行@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidaudit(LongorderId){// 只读查询,不加锁Orderorder=orderRepo.findById(orderId).orElseThrow();// 写入审计表(不同的表)AuditLogaudit=newAuditLog(orderId,"APPROVED");auditLogRepo.save(audit);}// 方案2:在 REQUIRES_NEW 调用前释放行锁(flush + 不再修改)@TransactionalpublicvoidmethodA(){Orderorder=orderRepo.findById(1L).orElseThrow();order.setStatus("PROCESSING");orderRepo.saveAndFlush(order);// flush 但不释放锁// 改为事务提交后再调用审计(避免死锁)// 或者审计方法操作不同的行/表}

5.3 连接池耗尽

// ❌ 危险:嵌套过深的 REQUIRES_NEW@Transactionalpublicvoidlevel1(){level2();// 新连接}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidlevel2(){level3();// 又一个新连接}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidlevel3(){level4();// 又一个新连接...}// 每层挂起的事务都持有一个连接// 4 层嵌套 = 4 个数据库连接被同一个线程占用// 并发量大时连接池迅速耗尽

规避方式:

  • 控制 REQUIRES_NEW 嵌套深度(建议不超过 2 层)
  • 尽量在最内层使用,不要级联嵌套

5.4 异常传播

@TransactionalpublicvoidmethodA(){try{newTxService.methodB();// REQUIRES_NEW,内部抛异常}catch(Exceptione){// T2 已回滚// 这里 catch 住了,T1 不会回滚log.warn("methodB 失败,继续执行 methodA",e);}// T1 继续正常提交}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidmethodB(){// T2 内部抛异常 → T2 回滚thrownewRuntimeException("业务异常");}

注意:如果 methodA 不 catch 异常,异常传播到 methodA 的事务边界时,T1 也会被标记为 rollback-only。


六、REQUIRES_NEW vs NESTED

维度REQUIRES_NEWNESTED
事务关系完全独立的新事务外层事务的子事务(保存点)
数据库连接使用新连接共用外层连接
外层回滚影响不影响内层(已提交)内层也回滚(保存点失效)
内层回滚影响不影响外层(catch 住)回滚到保存点,外层可继续
内层可见外层数据不可见(不同连接)可见(同一连接)
JPA 支持完全支持取决于实现(不是所有 JPA 实现都支持)
// NESTED:内层失败可回滚到保存点,外层继续@TransactionalpublicvoidmethodA(){saveOrder();// 保存订单try{nestedMethod();// NESTED 事务}catch(Exceptione){// 内层回滚到保存点// 外层 saveOrder() 的数据不受影响}saveLog();// 继续执行}@Transactional(propagation=Propagation.NESTED)publicvoidnestedMethod(){// 在保存点内执行// 失败只回滚这部分}

七、MQ 消费场景深入分析

为什么 MQ 消费方法适合 REQUIRES_NEW

// MQ 消费者框架代码(简化)@ComponentpublicclassMqConsumer{@RabbitListener(queues="my-queue")publicvoidonMessage(LonglogId){// 1. MQ 框架调用此方法时,通常没有外层事务// 2. 即使有框架层事务,业务也应该隔离try{// REQUIRES_NEW 保证明确的事务边界businessService.processInNewTx(logId);// 成功 → T2 已提交 → ACK 消息}catch(Exceptione){// 失败 → T2 已回滚 → NACK/重试log.warn("消费失败",e);}}}@ServicepublicclassBusinessService{@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessInNewTx(LonglogId){// 明确的独立事务:// - 成功:数据提交,状态更新为 Y// - 失败:数据回滚,状态保持 O/P}}

错误日志为什么也要 REQUIRES_NEW

@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessMessage(LonglogId){try{doBusinessLogic(logId);markSuccess(logId);}catch(Exceptione){// 问题:如果在当前事务中写错误日志,事务回滚后日志也丢了// 解决:错误日志写入方法也是 REQUIRES_NEWerrorLogService.saveError(logId,e);// 独立事务,不随本事务回滚throwe;// 重新抛出,让本事务回滚}}

与事务后置动作的配合

@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessMessage(LonglogId){// 业务逻辑...doWork();// 事务提交后才发送 MQ(保证数据可见性)TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronizationAdapter(){@OverridepublicvoidafterCommit(){// 此时 REQUIRES_NEW 的事务已提交// 新的消费者能读到本次写入的数据anotherMqSender.send(nextLogId);}});}

八、完整示例:MQ 消费 + 独立事务 + 错误处理

/** * MQ 消费者. * 职责:消息接收、锁控制、异常处理 * 不含业务逻辑。 */@Component@Slf4jpublicclassTaskMqConsumer{@ResourceprivateTaskProcessServicetaskProcessService;@ResourceprivateDistributedLockProviderlockProvider;@RabbitListener(queues="${mq.queue.task-process}")publicvoidconsume(LonglogId){log.info("收到消息, logId={}",logId);StringlockKey="task:process:"+logId;DistributedLocklock=lockProvider.getLock(lockKey,60,TimeUnit.SECONDS);if(!lock.tryLock(30,TimeUnit.SECONDS)){log.warn("获取锁失败, logId={}",logId);return;}try{// 调用独立事务的处理方法taskProcessService.processInNewTransaction(logId);}catch(Exceptione){// 事务已回滚,消息可能重试log.warn("任务处理失败, logId={}",logId,e);}finally{lock.unlock();}}}/** * 任务处理 Service. * REQUIRES_NEW 保证每次消费都是独立事务。 */@Service@Slf4jpublicclassTaskProcessService{@ResourceprivateTaskLogRepositorytaskLogRepository;@ResourceprivateErrorLogServiceerrorLogService;@ResourceprivateTaskProcessortaskProcessor;@ResourceprivateFollowUpMqSenderfollowUpMqSender;@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicvoidprocessInNewTransaction(LonglogId){// 1. 查询任务TaskLogtaskLog=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(newTransactionSynchronizationAdapter(){@OverridepublicvoidafterCommit(){followUpMqSender.send(taskLog.getFollowUpId());}});}catch(Exceptione){log.warn("任务执行失败, logId={}",logId,e);// 6. 错误日志独立保存(不受本事务回滚影响)errorLogService.saveError(logId,e.getMessage());throwe;// 本事务回滚}}}/** * 错误日志服务. * 独立事务保证错误信息不丢失。 */@ServicepublicclassErrorLogService{@ResourceprivateTaskLogRepositorytaskLogRepository;@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidsaveError(LonglogId,StringerrorMsg){TaskLogtaskLog=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)