# 分布式一致性与取舍 > 一致性不是「要不要强一致」,而是「哪个业务动作必须强一致、哪一段可以最终一致、不一致时怎么发现与修复」。绝大多数线上数据问题,不是选错模型,而是没有兜底。 ## 一、先把一致性说清楚 | 级别 | 含义 | 典型场景 | | --- | --- | --- | | 线性一致(强一致) | 任何时刻读到的都是最新已提交值 | 账户余额扣减、库存扣减 | | 顺序一致 | 所有节点看到相同顺序 | 日志、消息顺序 | | 因果一致 | 有因果关系的操作保序 | 评论与回复、订单与状态 | | 最终一致 | 一段时间后收敛 | 搜索索引、推荐、统计报表 | 讨论时最容易犯的错是笼统说「要强一致」。正确问法是:**哪个读操作、在什么时间窗内、必须看到哪个写的结果。** ## 二、CAP 与 PACELC CAP 说的是「网络分区发生时,在一致性与可用性之间选一个」。它常被误读成「三选二」,实际上**分区是常态、不是选项**。 更实用的是 PACELC:**分区时选 A 或 C;没有分区时,在延迟(L)与一致性(C)之间取舍。** 后者才是日常:跨机房同步复制保证强一致,代价是每次写多几十毫秒。 ## 三、跨服务事务的五种做法 | 方案 | 一致性 | 复杂度 | 适用 | | --- | --- | --- | --- | | 2PC / XA | 强一致 | 高,协调者单点,性能差 | 同库多资源、低频关键操作 | | TCC(Try-Confirm-Cancel) | 业务级强一致 | 高,要写三个接口与幂等 | 资金、库存等核心链路 | | Saga | 最终一致 | 中,要设计补偿 | 长流程、跨多个服务 | | 本地消息表 | 最终一致 | 低,靠定时投递 | 大多数业务通知、状态同步 | | 事务消息 | 最终一致 | 中,依赖消息中间件能力 | 与消息队列深度绑定的系统 | 选择逻辑:**钱和库存用 TCC 或单库事务;状态同步与通知用本地消息表;长流程用 Saga,但必须能补偿。** ## 四、幂等是前提,不是可选项 任何跨服务调用都可能重试,所以每个写接口都要幂等: - **唯一键**:业务单号 + 操作类型做唯一索引,重复插入直接冲突返回; - **去重表**:记录已处理的请求 ID,带过期时间; - **状态机**:只允许合法状态迁移(待支付 → 已支付),重复请求因状态不符被拒绝; - **版本号/乐观锁**:更新时带版本,冲突即失败重试。 幂等设计要在**接口契约**里写清楚:同一次请求重试 N 次,结果必须与 1 次相同。 ## 五、最终一致的必备件:对账与补偿 只要不是强一致,就一定有窗口期出错:消息丢、消费失败、重复消费、状态不同步。所以必须有: 1. **对账**:定时比对两边的关键状态(订单 vs 支付 vs 账务),发现差异; 2. **补偿**:差异自动修复(重发、回滚、人工工单); 3. **可观测**:差异量、修复量、未修复量做成指标,超过阈值告警; 4. **幂等重放**:补偿动作本身也要幂等,否则修一次错两次。 没有对账的「最终一致」,本质是「最终不知道一致没有」。 ## 六、两个典型场景 **场景一:下单扣库存** 库存是典型的「必须强一致」但又不该用分布式事务的资源。常见做法:库存服务内部用单库事务 + 乐观锁扣减;订单服务通过事务消息/本地消息表保证「下单成功必然触发扣减」;对账任务比对订单与库存流水,发现少扣就补偿。 **场景二:支付回调** 支付回调会重复、会乱序、会延迟。处理原则:回调只做「记录 + 幂等更新状态」,不做重业务逻辑;用支付单号做唯一键;状态机只允许单向迁移;未收到回调的订单靠主动查询兜底。 ## 七、自检清单 - [ ] 我能说清每类数据的一致性要求,而不是统一「强一致」 - [ ] 所有写接口都幂等,且契约里写明了幂等键 - [ ] 跨服务流程有补偿方案,补偿本身幂等 - [ ] 有对账任务,且差异可观测、可修复 - [ ] 知道消息可能重复、乱序、丢失,并有对应处理 - [ ] 状态迁移是单向的,非法迁移会被拒绝 - [ ] 能说出系统在「部分失败」时的最终状态与恢复路径