分布式系统中的最终一致性:Saga模式与TCC的工程实现对比

发布时间:2026/7/13 10:44:19
分布式系统中的最终一致性:Saga模式与TCC的工程实现对比 分布式系统中的最终一致性Saga模式与TCC的工程实现对比一、分布式事务的取舍之痛创业团队从单体制裁迁至微服务后第一个遇到的硬骨头就是分布式事务。订单服务创建了订单库存服务扣减了库存但支付服务超时了。数据不一致就这样产生了。二阶段提交2PC是传统方案但它在微服务架构中有致命问题锁资源时间长、单点故障阻塞、性能极差。工程实践中最终一致性方案才是主流选择。Saga和TCC是两大核心模式选型直接决定了数据一致性、代码复杂度和运维成本。行业案例一次订单超卖事故。某电商创业团队在促销活动期间订单服务调用 Saga 模式创建订单、扣减库存、生成物流单。前两步成功了第三步物流服务因第三方接口超时失败。Saga 补偿逻辑正确执行了取消物流单、恢复库存。但问题出在中间状态——库存显示为 0 持续了 3 秒这 3 秒内又进来了 12 个订单页面却显示已售罄。这是 Saga 的隔离性缺陷中间状态对并发用户可见。后续方案是库存服务在扣减接口上加分布式锁牺牲了部分并发换取了隔离性。另一个金融科技团队账户转账场景用了 Saga结果一次网络超时导致扣了用户余额但未完成转账补偿恢复余额时用户已经看到扣款记录投诉量暴增。金融场景下最终改用了 TCCTry 阶段冻结资金但不实际扣款Confirm 才执行扣款。代价是代码量翻倍但用户不再看到异常扣款记录。这两个案例的共同教训分布式事务模式的选择不是在真空中做要基于业务的容忍度和用户的可见性来决策。二、Saga与TCC的原理对比sequenceDiagram participant Order as 订单服务 participant Pay as 支付服务 participant Inv as 库存服务 participant Bus as 事件总线 Note over Order,Bus: Saga编排模式Orchestration Order-Pay: 创建支付 Pay--Order: 支付成功 Order-Inv: 扣减库存 Inv--xOrder: 库存不足 Order-Pay: 取消支付补偿 Pay--Order: 补偿成功 Order-Bus: 订单取消事件Saga将长事务拆分为多个本地事务。每个本地事务完成后发布事件触发下一个事务。如果某个步骤失败执行对应的补偿操作回滚。TCC则是两阶段的变体。Try阶段预留资源Confirm阶段提交Cancel阶段释放。关键区别在于TCC在Try阶段就锁定了资源而Saga不在执行阶段锁定。sequenceDiagram participant Svc as 协调器 participant Res as 资源服务 Svc-Res: Try预留资源 Res--Svc: 预留成功 Note over Svc: 等待所有Try完成 alt 全部成功 Svc-Res: Confirm确认提交 Res--Svc: 提交成功 else 任一失败 Svc-Res: Cancel释放资源 Res--Svc: 释放成功 end三、生产级实现3.1 Saga编排模式的实现import asyncio import json from typing import Dict, List, Callable, Any from dataclasses import dataclass, field from enum import Enum import logging logger logging.getLogger(__name__) class SagaStepStatus(Enum): Saga步骤状态 PENDING pending RUNNING running COMPLETED completed COMPENSATING compensating COMPENSATED compensated FAILED failed dataclass class SagaStep: Saga事务步骤定义 name: str # execute和compensate必须成对定义 # 为什么必须成对没有补偿步骤的Saga无法回滚等同于裸奔 execute: Callable # 执行函数 compensate: Callable # 补偿函数 status: SagaStepStatus SagaStepStatus.PENDING result: Any None class SagaOrchestrator: Saga编排器 核心职责按顺序执行步骤失败时反向补偿。 为什么选择编排模式而非协同模式 编排模式将流程逻辑集中在协调器中可读性和可维护性更高。 协同模式Choreography通过事件驱动分散在各服务中 流程逻辑散落各处调试困难。创业团队应对复杂业务时 编排模式更适合。 def __init__(self, saga_name: str): self.name saga_name self.steps: List[SagaStep] [] self.executed_steps: List[SagaStep] [] # 已执行步骤的出栈 def add_step(self, step: SagaStep) - None: 添加步骤。执行顺序即添加顺序。 self.steps.append(step) async def execute(self) - Dict: 执行Saga事务。 为什么用async分布式事务中的每个步骤通常涉及远程调用 同步等待会阻塞整个事务。异步模式可以配合超时控制。 logger.info(fSaga[{self.name}]: 开始执行共{len(self.steps)}步) for step in self.steps: try: logger.info(fSaga[{self.name}]: 执行步骤[{step.name}]) step.status SagaStepStatus.RUNNING step.result await step.execute() step.status SagaStepStatus.COMPLETED self.executed_steps.append(step) except Exception as e: logger.error( fSaga[{self.name}]: 步骤[{step.name}]执行失败: {e} ) step.status SagaStepStatus.FAILED # 步骤失败立即启动补偿流程 await self._compensate() return { success: False, error: str(e), failed_step: step.name, compensated_steps: len(self.executed_steps) } logger.info(fSaga[{self.name}]: 所有步骤执行成功) return {success: True, steps: len(self.executed_steps)} async def _compensate(self) - None: 反向补偿已执行的步骤。 为什么是反序补偿 后执行的步骤依赖先执行的步骤补偿必须反序撤销。 比如先扣库存后扣积分补偿时先还积分后还库存。 logger.info(fSaga[{self.name}]: 开始补偿共{len(self.executed_steps)}步) for step in reversed(self.executed_steps): try: logger.info(fSaga[{self.name}]: 补偿步骤[{step.name}]) step.status SagaStepStatus.COMPENSATING await step.compensate(step.result) step.status SagaStepStatus.COMPENSATED except Exception as e: # 补偿失败是最坏情况需要人工介入 # 为什么不能自动重试 # 补偿操作本身可能有副作用无限重试可能造成数据混乱 logger.critical( fSaga[{self.name}]: 补偿步骤[{step.name}]失败: {e}需要人工处理 ) # 发送告警到On-Call self._send_alert(step.name, str(e)) def _send_alert(self, step_name: str, error: str) - None: 补偿失败告警。生产环境对接PagerDuty或飞书。 # 实际实现应调用告警服务 pass3.2 TCC模式Try-Confirm-Cancel的实现from abc import ABC, abstractmethod from typing import Optional from redis import Redis import time class TCCResource(ABC): TCC资源接口 为什么需要frozen_timeout Try阶段预留的资源不能永久冻结超时后自动释放。 这是TCC的保底机制防止预留资源泄漏。 abstractmethod async def try_phase(self, biz_id: str, amount: int) - bool: Try: 预留资源 pass abstractmethod async def confirm_phase(self, biz_id: str) - bool: Confirm: 确认使用资源 pass abstractmethod async def cancel_phase(self, biz_id: str) - bool: Cancel: 释放预留资源 pass class InventoryTCCResource(TCCResource): 库存TCC资源实现 核心思路Try阶段不扣减真实库存而是扣减可锁定库存。 为什么区分真实库存和可锁定库存 允许多个TCC事务并发预留提高并发能力。 def __init__(self, redis: Redis): self.redis redis self.frozen_timeout 300 # 冻结超时5分钟 async def try_phase(self, biz_id: str, amount: int) - bool: 预留库存 # Lua脚本保证原子性 # 为什么用Lua脚本多步Redis操作需要原子执行 # 否则在高并发下可能出现超卖 lua_script local available tonumber(redis.call(GET, KEYS[1]) or 0) local frozen tonumber(redis.call(GET, KEYS[2]) or 0) local amount tonumber(ARGV[1]) -- 真实库存 - 已冻结库存 必须 请求量 if available - frozen amount then -- 增加已冻结库存 redis.call(INCRBY, KEYS[2], amount) -- 记录业务冻结明细用于超时释放 redis.call(SETEX, KEYS[3], ARGV[2], amount) return 1 end return 0 keys [ finv:real:{biz_id}, # 真实库存 finv:frozen:{biz_id}, # 已冻结库存 finv:frozen_detail:{biz_id} # 冻结明细 ] result self.redis.eval(lua_script, 3, *keys, amount, self.frozen_timeout) return result 1 async def confirm_phase(self, biz_id: str) - bool: 确认扣库存 lua_script local frozen_amount tonumber(redis.call(GET, KEYS[2]) or 0) -- 扣减真实库存并释放冻结 -- 为什么同时操作保持一致性 redis.call(DECRBY, KEYS[1], frozen_amount) redis.call(DEL, KEYS[2]) redis.call(DEL, KEYS[3]) return 1 keys [ finv:real:{biz_id}, finv:frozen:{biz_id}, finv:frozen_detail:{biz_id} ] self.redis.eval(lua_script, 3, *keys) return True async def cancel_phase(self, biz_id: str) - bool: 释放预留库存 lua_script local frozen_amount tonumber(redis.call(GET, KEYS[2]) or 0) -- 释放冻结恢复可锁定库存 redis.call(DEL, KEYS[2]) redis.call(DEL, KEYS[3]) return frozen_amount keys [ finv:real:{biz_id}, finv:frozen:{biz_id}, finv:frozen_detail:{biz_id} ] self.redis.eval(lua_script, 3, *keys) return True class TCCCoordinator: TCC事务协调器 为什么协调器需要幂等 网络重试时Confirm/Cancel可能被重复调用。 def __init__(self, resources: Dict[str, TCCResource]): self.resources resources async def execute(self, operations: list) - bool: 执行TCC事务。 两阶段流程 Phase 1: 所有Try全部成功 → Phase 2: Confirm Phase 1: 任一Try失败 → Phase 2: Cancel所有已Try的资源 # Phase 1: Try tried [] # 记录已Try的资源用于回滚 for op in operations: resource self.resources[op[resource]] success await resource.try_phase(op[biz_id], op[amount]) if not success: # Try失败Cancel所有已Try的资源 for t in reversed(tried): await self.resources[t].cancel_phase(op[biz_id]) return False tried.append(op[resource]) # Phase 2: Confirm for op in operations: resource self.resources[op[resource]] confirmed await resource.confirm_phase(op[biz_id]) if not confirmed: # Confirm失败理论上不应该发生但出现时全量Cancel # 注意这里可能产生不一致建议Confirm支持幂等重试 for t in reversed(tried): await self.resources[t].cancel_phase(op[biz_id]) return False return True四、边界权衡Saga的优缺点优点实现简单每个步骤独立开发松耦合适合跨服务、跨团队无锁竞争并发性能好缺点缺乏隔离性中间状态对用户可见补偿逻辑是业务代码易出错补偿失败需要人工介入TCC的优缺点优点隔离性好Try阶段锁定资源最终一致性有强保证适合金融等高一致性场景缺点代码侵入性强每个接口需要三个方法锁资源时间较长并发能力受限协调器成为瓶颈选型建议场景推荐方案理由电商订单、旅行预订Saga步骤多、跨服务、容忍中间态账户转账、积分扣除TCC资源锁定、强一致性审批流、通知分发Saga补偿成本低、流程驱动秒杀库存扣减TCC高并发、精确控制取舍决策何时从 Saga 升级到 TCC创业团队起步时几乎都选 Saga因为实现成本低。但有两个信号表明需要引入 TCC信号一中间态暴露给用户引起投诉。当 Saga 的补偿窗口从失败到补偿完成的时间内用户能看到不一致状态如余额显示错误、库存反复跳变且这类投诉开始影响 NPS 评分时需要引入 TCC 的 Try 阶段来预锁定状态。信号二补偿逻辑变得极其复杂。Saga 的美好在于补偿简单但如果补偿本身涉及多个系统调用、依赖顺序补偿成本超过了正面业务逻辑的成本说明 Saga 已不适合。此时 TCC 的两阶段锁定虽然侵入性强但每个阶段的逻辑更可控。迁移策略建议不是全系统切换到 TCC而是对特定资源引入 TCC 接口Saga 步骤中可以调用 TCC 资源的 Try/Confirm/Cancel。这样既保持了 Saga 编排的简洁性又保护了关键资源的一致性。混用是性价比最高的实践。五、总结Saga和TCC解决的是同一个问题的两个方向。Saga追求简洁和松耦合代价是中间状态可见。TCC追求强一致和隔离性代价是代码复杂度和锁等待。对于创业团队而言首选Saga。它的实现成本更低出问题时的排查路径更清晰。只有在涉及金钱、积分等不可妥协的场景时才引入TCC。关键在于选择一个模式后全线统一不要让同一个系统同时使用两种模式否则维护成本将成倍增长。