【Spring 事务传播机制】
Spring 事务传播机制本文聚焦于 Spring 事务传播机制的 7 种传播行为结合真实业务场景与代码示例帮助开发者在实际项目中做出正确的选择。目录一、为什么需要事务传播机制二、7 种传播行为全景图三、逐一深度解析1. REQUIRED默认2. REQUIRES_NEW3. SUPPORTS4. NOT_SUPPORTED5. MANDATORY6. NEVER7. NESTED四、传播行为选择决策树五、高频陷阱与避坑指南六、生产实践建议七、总结一、为什么需要事务传播机制在真实业务中Service 方法之间会相互调用。当一个带事务的方法调用另一个带事务的方法时就产生了一个核心问题这两个方法应该共用一个事务还是各自独立Spring 通过Transactional(propagation ...)来回答这个问题。Transactional(propagationPropagation.REQUIRED)publicvoidmethodA(){methodB();// methodB 该怎么处理事务}Spring 定义了 7 种传播行为本质上是在回答“当前已有事务时怎么办当前没有事务时怎么办”二、7 种传播行为全景图传播行为有事务时无事务时核心语义REQUIRED加入新建我需要事务有就用没有就建REQUIRES_NEW挂起旧的新建新建我要自己的事务不受外部影响SUPPORTS加入不建有事务就用没有也无所谓NOT_SUPPORTED挂起非事务运行非事务运行我不想要事务MANDATORY加入抛异常必须在事务中调用我NEVER抛异常非事务运行绝对不能有事务NESTED嵌套事务保存点新建我是外部事务的子事务三、逐一深度解析1. REQUIRED默认最常用行为有事务就加入没有就新建。真实场景电商下单流程下单涉及订单保存、库存扣减、支付记录创建三个操作必须保证原子性任何一步失败全部回滚。ServicepublicclassOrderService{AutowiredprivateInventoryServiceinventoryService;AutowiredprivatePaymentServicepaymentService;Transactional// 默认 REQUIRED开启事务publicvoidplaceOrder(Orderorder){orderRepository.save(order);inventoryService.deductStock(order.getProductId(),order.getQuantity());paymentService.createPayment(order.getId(),order.getAmount());}}ServicepublicclassInventoryService{Transactional// REQUIRED加入 placeOrder 的事务publicvoiddeductStock(LongproductId,intquantity){// 如果这里抛异常整个 placeOrder 都会回滚inventoryRepository.deduct(productId,quantity);}}三个操作共用同一个事务任何一步失败全部回滚保证数据一致性。常见陷阱同类内部调用事务失效ServicepublicclassOrderService{TransactionalpublicvoidplaceOrder(Orderorder){orderRepository.save(order);this.deductStock(order);// ❌ 直接调用 this绕过了 Spring 代理}TransactionalpublicvoiddeductStock(Orderorder){// 这里的 Transactional 不会生效inventoryRepository.deduct(order.getProductId(),order.getQuantity());}}根本原因Spring 事务基于 AOP 代理实现this.method()调用的是原始对象不经过代理。解决方案将方法拆分到不同 Service或通过ApplicationContext获取代理对象后调用。2. REQUIRES_NEW独立事务最容易误用行为无论外部是否有事务都挂起外部事务新建一个完全独立的事务。真实场景操作日志记录业务操作失败时审计日志仍然需要被记录下来不能因为业务回滚而丢失。ServicepublicclassUserService{AutowiredprivateAuditLogServiceauditLogService;TransactionalpublicvoidupdateUserBalance(LonguserId,BigDecimalamount){userRepository.updateBalance(userId,amount);// 记录审计日志必须用 REQUIRES_NEW// 即使 updateBalance 后续失败回滚日志也要保留auditLogService.log(userId,UPDATE_BALANCE,amount);if(amount.compareTo(BigDecimal.ZERO)0){thrownewBusinessException(余额不足);// 外部事务回滚但日志已在独立事务中提交}}}ServicepublicclassAuditLogService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidlog(LonguserId,Stringaction,Objectdata){// 独立事务外部事务回滚不影响这里auditLogRepository.save(newAuditLog(userId,action,data));}}事务时序updateUserBalance 事务A 开始 ├── userRepository.updateBalance() [事务A] ├── auditLogService.log() │ └── 挂起事务A │ └── 开启事务B独立 │ └── auditLogRepository.save() │ └── 提交事务B ✅ │ └── 恢复事务A └── 抛出异常 → 回滚事务A ❌事务B 已提交日志保留真实场景短信 / 邮件发送TransactionalpublicvoidregisterUser(Useruser){userRepository.save(user);// 短信一旦发出无法撤回用 REQUIRES_NEW 让记录独立提交smsService.sendWelcomeSms(user.getPhone());}性能警告REQUIRES_NEW会从连接池获取新连接频繁使用会导致连接池耗尽。不要在循环中使用。3. SUPPORTS最灵活行为有事务就加入没有事务就以非事务方式运行。真实场景查询方法的灵活适配ServicepublicclassReportService{Transactional(propagationPropagation.SUPPORTS,readOnlytrue)publicListOrderSummarygetOrderSummary(LonguserId){// 调用方有事务 → 加入事务保证读一致性// 调用方无事务 → 普通查询节省事务开销returnorderRepository.findSummaryByUserId(userId);}}// 场景1在事务中调用加入事务保证读一致性TransactionalpublicvoidprocessOrder(LonguserId){updateOrder(userId);ListOrderSummarysummaryreportService.getOrderSummary(userId);// 加入事务}// 场景2直接调用非事务查询性能更好publicvoidshowDashboard(LonguserId){ListOrderSummarysummaryreportService.getOrderSummary(userId);// 无事务}适用原则纯查询方法且对是否在事务中执行没有强要求时使用。4. NOT_SUPPORTED主动排斥事务行为挂起当前事务以非事务方式运行。真实场景批量数据导出ServicepublicclassExportService{// 导出大量数据不需要事务避免长事务锁表Transactional(propagationPropagation.NOT_SUPPORTED)publicvoidexportLargeData(ExportTasktask){// 即使调用方有事务这里也会挂起它// 避免长时间持有事务锁减少锁竞争ListDatadatadataRepository.findAll();fileService.writeToFile(data,task.getFilePath());}}真实场景调用外部系统ServicepublicclassThirdPartyService{// 调用外部 API不应该在事务中外部调用不受事务控制Transactional(propagationPropagation.NOT_SUPPORTED)publicStringcallExternalApi(Stringrequest){// 挂起事务避免事务长时间等待外部响应returnhttpClient.post(externalApiUrl,request);}}核心价值避免长事务。数据库事务持有时间越长锁竞争越激烈性能越差。5. MANDATORY强制要求事务行为必须在已有事务中调用否则抛出IllegalTransactionStateException。真实场景核心财务操作的安全保障ServicepublicclassAccountService{// 转账操作必须在事务中执行否则数据不一致Transactional(propagationPropagation.MANDATORY)publicvoidtransfer(LongfromId,LongtoId,BigDecimalamount){accountRepository.debit(fromId,amount);accountRepository.credit(toId,amount);}}// ✅ 正确调用方式调用方开启了事务TransactionalpublicvoidprocessTransfer(TransferRequestrequest){validateRequest(request);accountService.transfer(request.getFrom(),request.getTo(),request.getAmount());}// ❌ 错误调用方式没有事务直接抛出 IllegalTransactionStateExceptionpublicvoidwrongCall(TransferRequestrequest){accountService.transfer(request.getFrom(),request.getTo(),request.getAmount());}设计意图MANDATORY是一种防御性编程手段。它强制要求调用方必须开启事务防止开发者忘记加Transactional导致数据不一致。适用场景底层 DAO 或核心业务方法这些方法本身不负责开启事务但必须在事务保护下运行。6. NEVER禁止事务行为必须在没有事务的情况下运行如果有事务则抛出异常。真实场景只读缓存查询ServicepublicclassCacheService{// 从缓存读取绝对不应该在事务中Transactional(propagationPropagation.NEVER)publicObjectgetFromCache(Stringkey){returnredisTemplate.opsForValue().get(key);}}真实场景幂等性检查ServicepublicclassIdempotencyService{// 幂等性检查不应该在事务中避免锁竞争Transactional(propagationPropagation.NEVER)publicbooleanisProcessed(StringrequestId){returnidempotencyRepository.exists(requestId);}}实际使用频率NEVER在生产代码中较少见更多用于框架层面的约束或测试场景。7. NESTED嵌套事务最复杂行为如果有事务在当前事务内创建一个嵌套事务通过 JDBC Savepoint 实现。嵌套事务可以独立回滚但最终提交依赖外部事务。NESTED vs REQUIRES_NEW 核心区别特性REQUIRES_NEWNESTED事务独立性完全独立嵌套在外部事务中外部回滚影响内部不影响外部回滚内部也回滚内部回滚影响外部不影响不影响回到保存点数据库连接新连接同一连接实现机制新事务Savepoint真实场景批量处理允许部分失败ServicepublicclassBatchOrderService{TransactionalpublicBatchResultprocessBatchOrders(ListOrderorders){BatchResultresultnewBatchResult();for(Orderorder:orders){try{// 每个订单用嵌套事务处理// 单个订单失败不影响整批次的外部事务orderService.processSingleOrder(order);result.addSuccess(order.getId());}catch(Exceptione){// 嵌套事务回滚到保存点外部事务继续result.addFailure(order.getId(),e.getMessage());}}// 外部事务提交所有成功的订单returnresult;}}ServicepublicclassOrderService{Transactional(propagationPropagation.NESTED)publicvoidprocessSingleOrder(Orderorder){orderRepository.save(order);inventoryService.deductStock(order);// 如果这里失败只回滚到此嵌套事务的保存点// 不影响外部批次事务中已成功的其他订单}}事务时序外部事务开始 ├── 处理 Order#1 │ └── 创建 Savepoint SP1 │ └── save(order1) ✅ │ └── deductStock(order1) ✅ │ └── 释放 SP1成功 ├── 处理 Order#2 │ └── 创建 Savepoint SP2 │ └── save(order2) ✅ │ └── deductStock(order2) ❌ 抛异常 │ └── 回滚到 SP2order2 的操作撤销 │ └── 外部 catch 捕获继续循环 ├── 处理 Order#3 │ └── ... ✅ └── 外部事务提交Order#1、Order#3 成功提交注意NESTED依赖数据库对 Savepoint 的支持MySQL InnoDB 支持部分数据库不支持。四、传播行为选择决策树调用一个方法时问自己 1. 这个方法必须和调用方在同一事务中 ├── 是 → REQUIRED默认 └── 否 → 继续问 2. 这个方法需要独立提交不受外部回滚影响 ├── 是 → REQUIRES_NEW如日志、短信 └── 否 → 继续问 3. 这个方法是外部事务的子操作允许独立回滚但最终跟随外部 ├── 是 → NESTED如批量处理 └── 否 → 继续问 4. 这个方法不需要事务但有事务也无所谓 ├── 是 → SUPPORTS如只读查询 └── 否 → 继续问 5. 这个方法必须在事务中被调用否则是编程错误 ├── 是 → MANDATORY如核心财务操作 └── 否 → 继续问 6. 这个方法不能在事务中运行如长时间操作、外部调用 ├── 是 → NOT_SUPPORTED └── 否 → 这个方法绝对不能有事务 ├── 是 → NEVER └── 否 → 回到 REQUIRED五、高频陷阱与避坑指南陷阱 1异常类型影响回滚Transactional默认只回滚RuntimeException和Error受检异常不会触发回滚。// ❌ 危险写法捕获了受检异常事务不会回滚Transactionalpublicvoidmethod(){try{riskyOperation();}catch(IOExceptione){log.error(error,e);// 事务不会回滚}}// ✅ 正确写法指定 rollbackForTransactional(rollbackForException.class)publicvoidmethod()throwsIOException{riskyOperation();// 让异常传播出去触发回滚}陷阱 2REQUIRES_NEW 死锁风险Transactionalpublicvoidouter(){repository.updateRowA(id);// 持有 row_A 的锁inner();// 调用 REQUIRES_NEW}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidinner(){// 新事务也想获取 row_A 的锁// 外部事务持有锁但被挂起内部事务等待锁 → 死锁repository.updateRowA(id);}规避原则REQUIRES_NEW的内部事务不应操作外部事务已锁定的数据。陷阱 3NESTED 与 REQUIRES_NEW 混淆两者行为相似但语义完全不同选错会导致严重问题场景正确选择选错的后果日志/审计记录REQUIRES_NEW用NESTED外部回滚时日志也被回滚日志丢失批量处理子操作NESTED用REQUIRES_NEW外部回滚无法撤销已独立提交的子操作陷阱 4事务不跨线程传播Transactionalpublicvoidmethod(){// ❌ 开启新线程事务不会传播到新线程newThread(()-{repository.save(entity);// 这里没有事务保护}).start();}Spring 事务绑定在ThreadLocal上不跨线程传播。异步操作需要在新线程中重新开启事务。陷阱 5同类内部调用事务失效重申ServicepublicclassOrderService{TransactionalpublicvoidplaceOrder(Orderorder){this.deductStock(order);// ❌ 绕过代理Transactional 失效}Transactional(propagationPropagation.REQUIRES_NEW)publicvoiddeductStock(Orderorder){// 这里的传播行为不会生效}}解决方案ServicepublicclassOrderService{AutowiredprivateInventoryServiceinventoryService;// 拆分到独立 ServiceTransactionalpublicvoidplaceOrder(Orderorder){inventoryService.deductStock(order);// ✅ 经过代理传播行为生效}}六、生产实践建议建议说明默认用 REQUIRED不要过度设计大多数业务场景用默认值就够了不要为了灵活而滥用其他传播行为日志、审计、通知用 REQUIRES_NEW这类操作需要独立于业务事务确保业务失败时记录仍然保留批量处理考虑 NESTED允许部分失败、部分成功的批量场景NESTED比REQUIRES_NEW更合适不会创建新连接长操作用 NOT_SUPPORTED调用外部 API、生成报表、大批量查询主动挂起事务避免长事务锁表核心底层方法用 MANDATORY 做防御强制要求调用方必须开启事务把事务责任明确化始终指定 rollbackForTransactional(rollbackFor Exception.class)避免受检异常不回滚的隐患REQUIRES_NEW 不要在循环中使用每次调用都会获取新连接频繁使用会耗尽连接池七、总结Spring 事务传播机制的本质是事务边界的协商协议。理解它的关键不是背 7 种行为而是理解两个维度当前有事务时加入、新建、挂起、嵌套、报错当前无事务时新建、不建、报错在真实业务中的选择规律90% 的场景 → REQUIRED默认 日志 / 审计 → REQUIRES_NEW 批量处理 → NESTED 长操作 / 外部调用 → NOT_SUPPORTED掌握这四个核心场景基本覆盖了生产中的绝大多数需求。其余传播行为SUPPORTS、MANDATORY、NEVER更多用于框架设计和防御性编程按需选择即可。文档生成时间2026-05-25