行业资讯

Spring 事务传播机制与 REQUIRES_NEW

发布时间:2026/7/30 20:41:59
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(propagationPropagation.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 释放锁四、典型应用场景场景 1MQ 消费方法独立事务/** * MQ 消费者调用 Service 层的处理方法. * 为什么需要 REQUIRES_NEW * 1. MQ 框架调用消费方法时可能没有事务上下文 * 2. 需要明确的事务边界控制提交/回滚 * 3. 异常时只回滚本次业务操作不影响消费框架 */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessMessage(LonglogId){// 独立事务成功则提交失败则回滚TaskLogtaskLogtaskLogRepo.findById(logId).orElseThrow();businessService.doWork(taskLog);taskLog.setStatus(Y);taskLogRepo.saveAndFlush(taskLog);}场景 2错误日志独立保存Transactional(rollbackForException.class)publicvoidprocessOrder(LongorderId){try{doBusinessLogic(orderId);}catch(Exceptione){// 业务失败当前事务将回滚// 但错误日志必须持久化不能跟着回滚errorLogService.saveErrorLog(orderId,e.getMessage());throwe;}}// 错误日志服务ServicepublicclassErrorLogService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidsaveErrorLog(LongbizId,StringerrorMsg){// 独立事务即使外层事务回滚错误日志也能保存ErrorLoglognewErrorLog();log.setBizId(bizId);log.setErrorMsg(errorMsg);log.setCreateTime(newDate());errorLogRepo.save(log);}}场景 3部分操作需要立即可见Transactional(rollbackForException.class)publicvoidcreateOrderAndNotify(OrderDtodto){// 主事务创建订单尚未提交OrderordercreateOrder(dto);// 需要立即生成一个编号并让其他服务可查到StringseqNosequenceService.generateAndPersist(order.getId());// 继续主流程...order.setSeqNo(seqNo);orderRepo.save(order);}ServicepublicclassSequenceService{Transactional(propagationPropagation.REQUIRES_NEW)publicStringgenerateAndPersist(LongorderId){// 独立事务立即提交其他事务可以查到这个编号StringseqgenerateNextSeq();SeqRecordrecordnewSeqRecord(orderId,seq);seqRepo.saveAndFlush(record);returnseq;}}场景 4批量处理中的单条隔离ServicepublicclassBatchProcessor{ResourceprivateSingleItemProcessorsingleItemProcessor;/** * 批量处理每条记录独立事务. * 一条失败不影响其他记录。 */publicBatchResultprocessBatch(ListLongitemIds){BatchResultresultnewBatchResult();for(LongitemId:itemIds){try{singleItemProcessor.processOne(itemId);result.addSuccess(itemId);}catch(Exceptione){// 单条失败不中断批量result.addFailed(itemId,e.getMessage());}}returnresult;}}ServicepublicclassSingleItemProcessor{Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessOne(LongitemId){// 每条记录一个独立事务// 失败只回滚当前这条ItemitemitemRepo.findById(itemId).orElseThrow();doProcess(item);item.setStatus(DONE);itemRepo.save(item);}}五、注意事项与陷阱5.1 自调用失效问题ServicepublicclassOrderService{// ❌ 错误同类内部调用事务注解不生效TransactionalpublicvoidmethodA(){this.methodB();// 直接调用不走代理REQUIRES_NEW 失效}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){// 实际仍在 methodA 的事务中执行}}解决方案// 方案1注入自身代理ServicepublicclassOrderService{ResourceprivateOrderServiceself;// 注入代理对象TransactionalpublicvoidmethodA(){self.methodB();// 通过代理调用事务注解生效}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){...}}// 方案2拆分到不同 Service推荐ServicepublicclassOrderService{ResourceprivateOrderLogServiceorderLogService;TransactionalpublicvoidmethodA(){orderLogService.methodB();// 不同类走代理}}ServicepublicclassOrderLogService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){...}}5.2 死锁风险TransactionalpublicvoidmethodA(){// T1 锁定了 order 表 id1 的行OrderorderorderRepo.findByIdForUpdate(1L);order.setStatus(PROCESSING);orderRepo.save(order);// 行锁未释放T1 未提交// 调用 REQUIRES_NEW 方法auditService.audit(1L);// T2 开始}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidaudit(LongorderId){// T2 也要锁 order 表 id1 的行OrderorderorderRepo.findByIdForUpdate(orderId);// 死锁// T2 等待 T1 释放行锁// T1 等待 T2 完成才能继续}规避方式// 方案1REQUIRES_NEW 方法不锁相同行Transactional(propagationPropagation.REQUIRES_NEW)publicvoidaudit(LongorderId){// 只读查询不加锁OrderorderorderRepo.findById(orderId).orElseThrow();// 写入审计表不同的表AuditLogauditnewAuditLog(orderId,APPROVED);auditLogRepo.save(audit);}// 方案2在 REQUIRES_NEW 调用前释放行锁flush 不再修改TransactionalpublicvoidmethodA(){OrderorderorderRepo.findById(1L).orElseThrow();order.setStatus(PROCESSING);orderRepo.saveAndFlush(order);// flush 但不释放锁// 改为事务提交后再调用审计避免死锁// 或者审计方法操作不同的行/表}5.3 连接池耗尽// ❌ 危险嵌套过深的 REQUIRES_NEWTransactionalpublicvoidlevel1(){level2();// 新连接}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidlevel2(){level3();// 又一个新连接}Transactional(propagationPropagation.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(propagationPropagation.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(propagationPropagation.NESTED)publicvoidnestedMethod(){// 在保存点内执行// 失败只回滚这部分}七、MQ 消费场景深入分析为什么 MQ 消费方法适合 REQUIRES_NEW// MQ 消费者框架代码简化ComponentpublicclassMqConsumer{RabbitListener(queuesmy-queue)publicvoidonMessage(LonglogId){// 1. MQ 框架调用此方法时通常没有外层事务// 2. 即使有框架层事务业务也应该隔离try{// REQUIRES_NEW 保证明确的事务边界businessService.processInNewTx(logId);// 成功 → T2 已提交 → ACK 消息}catch(Exceptione){// 失败 → T2 已回滚 → NACK/重试log.warn(消费失败,e);}}}ServicepublicclassBusinessService{Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessInNewTx(LonglogId){// 明确的独立事务// - 成功数据提交状态更新为 Y// - 失败数据回滚状态保持 O/P}}错误日志为什么也要 REQUIRES_NEWTransactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessMessage(LonglogId){try{doBusinessLogic(logId);markSuccess(logId);}catch(Exceptione){// 问题如果在当前事务中写错误日志事务回滚后日志也丢了// 解决错误日志写入方法也是 REQUIRES_NEWerrorLogService.saveError(logId,e);// 独立事务不随本事务回滚throwe;// 重新抛出让本事务回滚}}与事务后置动作的配合Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessMessage(LonglogId){// 业务逻辑...doWork();// 事务提交后才发送 MQ保证数据可见性TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronizationAdapter(){OverridepublicvoidafterCommit(){// 此时 REQUIRES_NEW 的事务已提交// 新的消费者能读到本次写入的数据anotherMqSender.send(nextLogId);}});}八、完整示例MQ 消费 独立事务 错误处理/** * MQ 消费者. * 职责消息接收、锁控制、异常处理 * 不含业务逻辑。 */ComponentSlf4jpublicclassTaskMqConsumer{ResourceprivateTaskProcessServicetaskProcessService;ResourceprivateDistributedLockProviderlockProvider;RabbitListener(queues${mq.queue.task-process})publicvoidconsume(LonglogId){log.info(收到消息, logId{},logId);StringlockKeytask:process:logId;DistributedLocklocklockProvider.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 保证每次消费都是独立事务。 */ServiceSlf4jpublicclassTaskProcessService{ResourceprivateTaskLogRepositorytaskLogRepository;ResourceprivateErrorLogServiceerrorLogService;ResourceprivateTaskProcessortaskProcessor;ResourceprivateFollowUpMqSenderfollowUpMqSender;Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidprocessInNewTransaction(LonglogId){// 1. 查询任务TaskLogtaskLogtaskLogRepository.findById(logId).orElse(null);if(taskLognull){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(propagationPropagation.REQUIRES_NEW)publicvoidsaveError(LonglogId,StringerrorMsg){TaskLogtaskLogtaskLogRepository.findById(logId).orElse(null);if(taskLognull)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 或 readOnlytrue