多线程 + 事务传播误用导致的问题
典型且具有代表性的 SaaS OA 系统中 多线程 + 事务传播误用 导致问题,最终通过 PROPAGATION_REQUIRES_NEW
解决的场景。
🧩 背景:SaaS OA 系统的“批量审批”功能
- 系统类型:SaaS 架构的 OA 系统,支持多个租户(Tenant)。
- 功能需求:管理员可以批量审批多个请假申请(Batch Approve Leave Requests)。
- 性能要求:为了提升审批速度,系统使用 多线程并行处理 每个请假单。
- 数据一致性要求:
- 每个请假单的审批要记录日志。
- 审批成功后要更新请假单状态,并通知 HR 系统。
- 即使某个请假单处理失败,也不能影响其他请假单的提交。
❌ 问题场景:事务传播属性误用导致报错
原始错误代码(使用 PROPAGATION_REQUIRED
)
@Service
public class BatchApprovalService {@Autowiredprivate LeaveService leaveService;@Autowiredprivate ApprovalLogService approvalLogService;// 外层方法开启事务@Transactionalpublic void batchApprove(List<Long> leaveIds, String approver) {List<CompletableFuture<Void>> futures = new ArrayList<>();for (Long leaveId : leaveIds) {// 使用线程池并行处理CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {// ❌ 问题所在:每个线程内调用的方法也使用 REQUIREDleaveService.approveLeave(leaveId, approver);} catch (Exception e) {log.error("审批请假单 {} 失败", leaveId, e);// 不抛出,避免阻塞其他任务}}, taskExecutor); // 自定义线程池futures.add(future);}// 等待所有任务完成CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();}
}@Service
public class LeaveService {@Autowiredprivate LeaveMapper leaveMapper;@Autowiredprivate ApprovalLogService approvalLogService;// 使用默认 REQUIRED,依赖外层事务@Transactionalpublic void approveLeave(Long leaveId, String approver) {// 1. 查询请假单Leave leave = leaveMapper.selectById(leaveId);if (leave == null) throw new RuntimeException("请假单不存在");// 2. 更新状态leave.setStatus("APPROVED");leave.setApprover(approver);leaveMapper.updateById(leave);// 3. 记录审批日志(关键!)ApprovalLog log = new ApprovalLog(leaveId, approver, "APPROVED");approvalLogService.saveLog(log); // 内部也是 REQUIRED}
}@Service
public class ApprovalLogService {@Autowiredprivate ApprovalLogMapper logMapper;@Transactional // 默认 REQUIREDpublic void saveLog(ApprovalLog log) {logMapper.insert(log);}
}
⚠️ 报错现象
运行时出现如下错误:
org.springframework.transaction.IllegalTransactionStateException:
Participating in existing transaction with name [xxx], but configured with propagation 'REQUIRES_NEW'.
However, this error is more subtle...// 更常见的是:
java.lang.IllegalStateException:
Transaction synchronization is not activeat org.springframework.transaction.support.AbstractTransactionSynchronizationManager.getResource(AbstractTransactionSynchronizationManager.java:132)
或者:
“No transaction aspect-managed TransactionStatus in scope”
🔍 根本原因分析
- 外层
@Transactional
开启事务 → 事务绑定在 主线程 的ThreadLocal
中。 CompletableFuture.runAsync
创建新线程 → 子线程无法继承主线程的事务上下文。- 子线程中调用
@Transactional
方法(approveLeave
)→ Spring 尝试加入“当前事务”,但子线程没有事务上下文 → 事务失效或报错。 - 即使不报错,一旦某个子线程抛异常,整个外层事务可能回滚,导致其他已成功处理的请假单也被回滚,违反“独立处理”需求。
✅ 正确解决方案:使用 PROPAGATION_REQUIRES_NEW
修改 LeaveService.approveLeave
方法的事务传播行为:
@Service
public class LeaveService {@Autowiredprivate LeaveMapper leaveMapper;@Autowiredprivate ApprovalLogService approvalLogService;// ✅ 关键修改:使用 REQUIRES_NEW@Transactional(propagation = Propagation.REQUIRES_NEW)public void approveLeave(Long leaveId, String approver) {try {Leave leave = leaveMapper.selectById(leaveId);if (leave == null) throw new RuntimeException("请假单不存在");leave.setStatus("APPROVED");leave.setApprover(approver);leaveMapper.updateById(leave);ApprovalLog log = new ApprovalLog(leaveId, approver, "APPROVED");approvalLogService.saveLog(log); // 这个方法也应是 REQUIRES_NEW 或 REQUIRED} catch (Exception e) {// ✅ 即使失败,只回滚当前事务,不影响其他线程log.error("审批请假单 {} 失败", leaveId, e);throw e; // 抛出,由上层记录}}
}
同时,外层方法不再需要事务(或可以去掉):
@Service
public class BatchApprovalService {@Autowiredprivate LeaveService leaveService;// ❌ 不再需要 @Transactional// 因为每个子任务独立提交public void batchApprove(List<Long> leaveIds, String approver) {List<CompletableFuture<Void>> futures = new ArrayList<>();for (Long leaveId : leaveIds) {CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {// 每个调用都是独立事务leaveService.approveLeave(leaveId, approver);} catch (Exception e) {log.error("审批失败,但不影响其他", e);// 可记录失败日志}}, taskExecutor);futures.add(future);}CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();}
}
✅ 为什么 REQUIRES_NEW
能解决问题?
问题 | REQUIRED 表现 | REQUIRES_NEW 表现 |
---|---|---|
事务上下文继承 | 子线程无事务上下文 → 报错或失效 | 忽略外层,自己开启新事务 → ✅ 成功 |
异常影响范围 | 一个失败 → 外层回滚 → 全部失败 | 一个失败 → 仅自己回滚 → ❌ 其他继续 |
性能与隔离 | 所有操作串行等待事务 | 每个线程独立提交 → ✅ 高并发 |
日志可靠性 | 日志可能随事务回滚丢失 | 日志独立提交 → ✅ 永久记录 |
🏁 最终效果
- ✅ 每个请假单在独立线程中处理,互不影响。
- ✅ 每个处理过程有独立事务,提交或回滚不影响他人。
- ✅ 审批日志可靠记录,即使业务失败也不会丢失。
- ✅ 符合 SaaS 系统中“租户/任务隔离”的设计原则。
- ✅ 系统吞吐量提升,用户体验更好。
📌 总结:SaaS + 多线程 + 事务的经典模式
// 外层:不加事务,或只用于非核心操作
public void batchProcess(...) {for (...) {CompletableFuture.runAsync(() -> {// 内层:每个任务使用 REQUIRES_NEWservice.processInNewTransaction(...);});}
}
核心思想:在多线程异步处理中,避免共享事务上下文,使用
PROPAGATION_REQUIRES_NEW
实现任务级别的事务隔离与可靠性,是 SaaS 系统中高并发、高可用场景的推荐实践。