
分布式事务的一致性边界从 Saga 模式到 TCC 补偿的工程抉择一、微服务拆分后的数据一致性鸿沟单体应用中数据一致性由数据库事务保证——一条Transactional注解就能覆盖所有写操作。但微服务拆分后一个业务操作可能跨越多个服务每个服务拥有独立的数据库。本地事务无法跨越服务边界分布式一致性成为必须直面的问题。以电商的下单流程为例创建订单、扣减库存、扣减用户余额、发放优惠券这四个操作分布在四个服务中。任何一个操作失败都需要回滚已执行的操作。但分布式环境下回滚本身也可能失败——库存扣减成功了但余额扣减超时此时库存是回滚还是保留如果保留用户没付款却占了库存如果回滚但回滚请求也超时了呢这就是分布式事务的核心困境在不可靠的网络和不可靠的节点上如何保证多个服务间的数据最终一致。两阶段提交2PC虽然能保证强一致性但其阻塞特性在微服务场景下不可接受——任何一个参与者的阻塞都会导致整个事务挂起。Saga 和 TCC 是两种主流的非阻塞方案但各自的适用场景和工程代价差异显著。二、Saga 与 TCC 的补偿机制两种一致性模型的执行链路Saga 模式将长事务拆分为多个本地事务每个本地事务都有对应的补偿操作。如果某个步骤失败则逆序执行已完成步骤的补偿操作。TCCTry-Confirm-Cancel则为每个操作定义三个阶段Try 预留资源、Confirm 确认执行、Cancel 释放资源。flowchart TD A[订单服务创建订单] -- B[库存服务扣减库存] B -- C[账户服务扣减余额] C -- D{余额是否充足?} D --|充足| E[确认所有操作] D --|不足| F[触发补偿链路] F -- G[账户服务恢复余额] G -- H[库存服务恢复库存] H -- I[订单服务取消订单] subgraph Saga 模式 A -- B -- C -- D F -- G -- H -- I end subgraph TCC 模式 J[Try: 冻结余额] -- K[Try: 预占库存] K -- L{资源是否可用?} L --|可用| M[Confirm: 扣减冻结余额] M -- N[Confirm: 确认扣减库存] L --|不可用| O[Cancel: 解冻余额] O -- P[Cancel: 释放预占库存] endSaga 和 TCC 的关键区别在于补偿与撤销的语义差异。Saga 的补偿是语义上的反向操作——扣减库存的补偿是增加库存但增加的库存和扣减的库存可能不是同一批如批次号不同。TCC 的 Cancel 则是释放预留资源——冻结的余额直接解冻预占的库存直接释放语义上更精确。这种差异决定了两者的适用场景Saga 适合业务语义明确、补偿操作容易定义的场景TCC 适合资源预留语义清晰、对一致性要求更高的场景。三、Saga 编排器的生产级实现3.1 基于 Spring Statemachine 的 Saga 编排/** * Saga 编排器基于状态机驱动分布式事务的执行与补偿 * 核心思路将 Saga 的每个步骤定义为状态机的状态转换 * 步骤失败时自动触发补偿链路 */ Service public class OrderSagaOrchestrator { private final StateMachineFactoryOrderState, OrderEvent factory; private final InventoryService inventoryService; private final AccountService accountService; private final CouponService couponService; /** * 执行下单 Saga创建订单 → 扣减库存 → 扣减余额 → 发放优惠券 * 每个步骤失败时逆序执行已完成步骤的补偿操作 */ public SagaResult execute(OrderRequest request) { SagaContext context new SagaContext(request); ListCompensableAction completedActions new ArrayList(); try { // 步骤1创建订单 Order order orderService.create(request); context.setOrder(order); completedActions.add(() - orderService.cancel(order.getId())); // 步骤2扣减库存 InventoryResult invResult inventoryService.deduct( request.getProductId(), request.getQuantity()); if (!invResult.isSuccess()) { throw new SagaException(库存扣减失败: invResult.getReason()); } completedActions.add(() - inventoryService.restore( request.getProductId(), request.getQuantity())); // 步骤3扣减余额 AccountResult accResult accountService.deduct( request.getUserId(), order.getTotalAmount()); if (!accResult.isSuccess()) { throw new SagaException(余额扣减失败: accResult.getReason()); } completedActions.add(() - accountService.restore( request.getUserId(), order.getTotalAmount())); // 步骤4发放优惠券 couponService.issue(request.getUserId(), order.getId()); return SagaResult.success(order); } catch (SagaException e) { // 逆序执行补偿操作 compensate(completedActions); return SagaResult.failure(e.getMessage()); } } /** * 逆序执行补偿操作 * 关键设计补偿操作本身也可能失败需要记录失败状态并重试 */ private void compensate(ListCompensableAction actions) { // 逆序遍历先补偿最后完成的操作 for (int i actions.size() - 1; i 0; i--) { try { actions.get(i).compensate(); } catch (Exception e) { // 补偿失败记录到补偿表由定时任务重试 // 这是 Saga 模式中最关键的设计——补偿必须最终成功 logCompensationFailure(actions.get(i), e); scheduleCompensationRetry(actions.get(i)); } } } /** * 补偿重试调度器指数退避重试最大重试次数 10 次 * 超过最大重试次数后标记为需要人工介入 */ Scheduled(fixedDelay 30000) public void retryFailedCompensations() { ListCompensationRecord failedRecords compensationRepository.findPendingRecords(); for (CompensationRecord record : failedRecords) { if (record.getRetryCount() 10) { record.setStatus(CompensationStatus.MANUAL_INTERVENTION); compensationRepository.save(record); // 发送告警通知 alertService.notifyCompensationStuck(record); continue; } try { // 执行补偿 executeCompensation(record); record.setStatus(CompensationStatus.COMPLETED); } catch (Exception e) { record.incrementRetryCount(); record.setNextRetryAt(calculateNextRetry(record.getRetryCount())); } compensationRepository.save(record); } } /** * 指数退避计算1min, 2min, 4min, 8min, ... */ private Instant calculateNextRetry(int retryCount) { long delayMinutes (long) Math.pow(2, retryCount); return Instant.now().plus(delayMinutes, ChronoUnit.MINUTES); } }3.2 补偿幂等性保障/** * 补偿操作的幂等性包装器 * 核心思路通过唯一事务 ID 步骤序号保证补偿操作只执行一次 * 即使补偿请求被重复发送也不会产生副作用 */ Component public class IdempotentCompensationWrapper { private final CompensationLogRepository logRepository; /** * 包装补偿操作添加幂等性检查 * param transactionId 全局事务 ID * param stepIndex 步骤序号 * param action 补偿操作 */ public void executeIdempotently(String transactionId, int stepIndex, Runnable action) { String compensationKey transactionId :compensate: stepIndex; // 检查是否已执行过补偿 if (logRepository.existsByKey(compensationKey)) { log.info(补偿操作已执行跳过: {}, compensationKey); return; } // 执行补偿 action.run(); // 记录补偿完成状态 logRepository.save(new CompensationLog( compensationKey, Instant.now(), CompensationStatus.COMPLETED )); } }补偿操作的幂等性是 Saga 模式的生命线。网络超时导致的重试、消息队列的重复投递都可能触发重复补偿。如果补偿操作不是幂等的如增加库存被重复执行两次就会导致数据不一致。四、Saga 与 TCC 的工程代价与适用边界两种方案各有其工程代价选择时需要结合业务特性权衡。Saga 的补偿语义模糊问题。Saga 的补偿是语义反向操作而非物理撤销。扣减库存的补偿是增加库存但增加的库存和原始库存可能属性不同如有效期、批次。在库存管理严格的场景中如药品、食品这种语义差异可能导致合规问题。TCC 的资源冻结成本。TCC 的 Try 阶段需要冻结资源被冻结的资源在 Confirm 或 Cancel 之前不可用。如果事务持续时间较长如等待人工审批大量资源被冻结会严重影响系统的可用性。TCC 不适合长事务。空回滚与悬挂问题。TCC 模式中Try 请求可能因为网络超时而未到达参与者但 Cancel 请求先到达了。参与者收到 Cancel 时没有对应的 Try 记录这就是空回滚。更棘手的是悬挂——Cancel 执行后Try 请求才到达参与者执行了 Try 但没有对应的 Confirm/Cancel。这两个问题都需要通过事务状态表和超时检测来解决。适用边界建议对于补偿语义清晰、业务可容忍短期不一致的场景如电商下单Saga 更简单、侵入性更低。对于资源预留语义明确、一致性要求严格的场景如金融转账TCC 更可靠。无论选择哪种方案都必须实现补偿/取消的幂等性和重试机制否则分布式事务的最终一致就无法保证。五、总结分布式事务的本质是在不可靠的分布式环境中用补偿机制替代原子提交。Saga 用正向执行 逆向补偿的模式以最终一致性换取了系统的可用性。TCC 用预留 确认/取消的模式在资源层面实现了更精确的一致性控制。落地路线上建议从 Saga 模式起步它的侵入性更低实现成本更可控。先实现核心链路的补偿操作和幂等性保障再逐步完善补偿重试和告警机制。当业务场景对一致性要求提升到 Saga 无法满足时再针对关键链路引入 TCC 模式。两种模式可以在同一个系统中并存按业务特性选择合适的一致性级别。