3258 字
约 10 分钟
15
S18-B05 · 一致性的基本模型:强一致、最终一致与补偿

第一次遇到"数据不一致"通常是在对账时:订单显示已支付,账务没入账;库存减了,订单却超时取消。查日志每一步都成功,只是没人在同一个事务里。

单库时代数据库事务兜住了这件事,一旦拆库、加缓存、上消息队列,兜底的人就没了。这篇把一致性拆成三个可判断的问题:谁必须立刻看见这次修改,谁可以晚点看见,晚点的那部分谁负责纠正。


1. 一致性到底在说什么

一致性不是"数据对不对",而是:多个副本(或多个服务)对同一个事实的看法,是否相同、什么时候相同。

说法 精确含义 反例
"数据不一致" 两个地方对同一事实给出不同答案 订单已支付,账务未入账
"数据错了" 值本身违反业务规则 金额为负
"延迟" 最终一样,但此刻不一样 缓存里还是旧价

区分这三者很重要:"错"修数据,"不一致"修流程,"延迟"调预期。很多团队把延迟当错误去追一个本来就会被追上的副本。


2. 强一致与最终一致:差别在于"什么时候"

维度 强一致 最终一致
读到的保证 任何时刻读到的都是最新已提交值 一段时间后读到最新值,中间可能读到旧值
实现手段 单库事务、分布式共识(多数派确认)、全局锁 异步复制、消息投递、事件驱动
协调成本 每次写都要多数节点确认 写本地成功即可返回
延迟量级 随副本距离与节点数增加 写路径短,读路径可能读到旧值
可用性 分区时可能拒绝写入 分区时仍可写,事后合并
失败后的状态 要么成功要么没有 存在"半成功",需要补偿

关键认知:强一致不是"更好",是"更贵"——买的是"读到的不会错",代价是写要等、分区时可能不写;最终一致买的是"随时可写、延迟低",代价是要有补偿机制。

一个系统里两种都会用:余额、库存扣减通常强一致;头像、点赞数、搜索索引通常最终一致。


3. 三种中间强度:大多数业务真正需要的

"强"与"最终"之间还有三档,它们比强一致便宜得多,又比"什么都可能"安全。

模型 保证 典型用途
读己之写 自己写完后,自己一定能读到自己的修改 用户提交表单后跳转详情页
单调读 不会读到比之前更旧的数据(时间不倒流) 分页浏览、刷新列表
因果一致 有因果关系的操作按顺序可见 评论与它引用的帖子

读己之写最常见也最便宜:把刚写的数据标记"该用户需读主库",或写后顺手更新本地缓存。它解决的是"用户觉得系统丢了数据"这类投诉。单调读要防的是界面"倒退"——从主库读到新值、刷新一次读到旧值;做法是会话粘同一副本,或按版本号拒绝回退。


4. 为什么分布式下的强一致难

障碍 表现 后果
网络分区 两个机房互相看不见,但都活着 要么拒绝写,要么双写后再合并冲突
节点故障 确认写到一半宕机 无法确定"到底写没写成功"
时钟不可靠 各机器时间漂移,now() 不可比 用时间戳判先后会错序
重试与超时 调用方超时,服务端仍在执行 出现重复执行与半成功

第三条最反直觉:"最后写赢"依赖时钟,而时钟不可靠;替代品是逻辑时钟、版本号,或让同一实体固定路由到同一节点。

第四条决定了一个事实:任何跨进程调用都可能是"结果未知",所以第一块砖不是共识算法,是幂等与去重。


5. 从一个请求出发的判断法

不用先选框架,先回答三个问题:

一致性三问(对每一个写操作问一次)
[ ] 这次修改必须被谁立刻看到?
      只有自己 → 读己之写;所有人 → 强一致;无所谓 → 最终一致
[ ] 允许多久不一致?给出具体数字(秒 / 分钟 / 天)
[ ] 不一致由谁纠正?重试、对账、还是人工?
      答不出"谁"→ 这个操作现在就是裸奔状态

第二个问题必须落成数字:"尽快"不是答案,"5 秒内"才是,它决定用双写还是异步投递。第三个问题指向补偿:一致性不是"保证不发生不一致",而是"保证不一致会被发现并修正"。

落地时逐条对一遍:

[ ] 每个写操作都回答了"谁必须立刻看到"
[ ] 允许多久不一致,写成了具体数字
[ ] 有幂等键,且去重标记与业务同事务提交
[ ] 有对账任务,差额有处理入口与责任人
[ ] 补偿动作本身也是幂等的

6. 补偿的三种手段

手段 适用 实现要点 局限
重试 瞬时失败(网络抖动、限流、超时) 指数退避、上限次数、幂等 逻辑性失败重试无用
对账 双方各有账本,定期比对差额 有唯一业务号、能拉全量或增量 发现有时延,需要人工或自动修
人工兜底 金额大、规则复杂、自动修复风险高 工单 + 可回滚的操作入口 成本高,不能作为主路径

三者叠加而非替代:重试处理瞬时问题,对账发现遗漏,人工处理歧义。

最小闭环
写路径:本地事务 + 幂等键(防重复)
投递路径:消息 + 重试 + 死信
核对路径:定时对账任务,比对业务号与金额
兜底路径:差额进人工工单,附原始上下文

没有第三行,等于相信消息永远不会丢。


7. Saga 与 TCC:两种补偿风格

Saga:把长事务拆成一串本地事务,某步失败就逆序执行已成功步骤的补偿动作。

下单 Saga
T1 创建订单      → C1 取消订单
T2 扣减库存      → C2 归还库存
T3 扣减余额      → C3 退回余额
若 T3 失败:执行 C2、C1(逆序)

要点:补偿动作必须幂等(补偿本身也会重试);Saga 没有隔离性,中间态对外可见(别人能看到"订单已建、库存已扣、余额未扣"),所以它适合长流程、能容忍中间态的业务。

TCC(Try-Confirm-Cancel):先预留资源(Try),全部成功再确认(Confirm),任一失败则取消(Cancel)。

对比 Saga TCC
资源占用 提交时才占用,中间不锁 Try 阶段就预留,占用时间长
隔离性 无,中间态可见 有,预留后别人拿不到
实现成本 低,只需补偿逻辑 高,要写三个接口且都幂等
适合 跨服务长流程、可容忍中间态 资源紧张、必须防超卖的场景

入门阶段先用 Saga,真出现超卖或资源被抢占再考虑 TCC。


8. 幂等与去重:一致性的地基

重试、消息重投、Saga 补偿——所有补偿机制都会重复执行。如果没有幂等,补偿就等于放大器。

// 幂等:以业务号作为去重键,先查后做,结果也缓存
public void deduct(String bizNo, Money amount) {
    if (!dedup.tryAcquire(bizNo)) return;        // 已处理过,直接返回
    try {
        account.deduct(amount);                   // 本地事务内
        dedup.markDone(bizNo);                    // 与业务同事务提交
    } catch (Exception e) {
        dedup.release(bizNo);                     // 失败释放,允许重试
        throw e;
    }
}

两个细节决定成败:去重标记必须与业务操作同事务提交,否则"业务成功、标记丢失"会造成二次扣减;去重窗口要覆盖最长重试跨度,典型量级从几分钟到几天,不能随手设成 1 小时。


9. 把技术保证翻译成产品表述

技术保证 可以直接对用户说的 不要说的
强一致 "支付成功后余额立即更新" "绝对不会有延迟"
最终一致(秒级) "最多几秒后各处显示一致" "实时同步"
最终一致(分钟级) "数据会在几分钟内汇总完成" "马上就能看到"
需要人工兜底 "异常订单会在 1 个工作日内处理" "系统会自动修复"

产品表述的价值是给不一致留出解释空间:说"实时"却偶尔延迟 30 秒,用户会认为系统坏了;说"几秒内"就不会。


10. 常见错误

错误 后果 改法
默认所有操作都要强一致 全链路加锁、延迟升高、分区时不可用 逐个操作按第 5 节三问分类
用重试代替补偿 逻辑性失败被重试成雪崩 区分瞬时失败与业务失败
没有对账机制 差额只能等用户投诉才发现 加定时对账,按业务号比对
把最终一致当"不用管" 不一致永久留存,越滚越大 写明纠正责任人与时限
补偿动作不幂等 补偿重试造成二次扣减 补偿同样走幂等键

11. 排错:五种"数据对不上"的现场

现象 常见原因 处理顺序
两边金额差一笔 消息丢失或消费失败 先查消费日志与死信,再对账补数
重复扣款 重试无幂等或去重标记未同事务 补幂等键,核对去重表写入时机
用户看不到自己刚提交的数据 写主库、读从库 读己之写:短窗口内读主库
界面数据"倒退" 跨副本读到旧版本 会话粘副本或按版本号拒绝回退

通用顺序:先确定"谁的事实是准的",再找差额来源,最后才修数据;顺序反过来会把正确数据改错。


12. 边界:什么时候不必上分布式一致性方案

场景 做法 理由
同一库、同一事务内能完成 直接本地事务 强一致最便宜的形式就是单库事务
只读展示、允许旧值 加 TTL 缓存即可 用户感知不到秒级差异
数据可重算(报表) 定期全量重算 不必维护增量一致性
尚未拆分(单体单库) 不引入 Saga/TCC 用事务解决,别提前付分布式的账

一句话:一致性的成本按"不一致的实际损失"来付;损失小的地方,复杂方案只是自造故障面。


13. 自测

  1. "数据不一致"与"数据错了"有什么区别?两者分别该修什么?
  2. 强一致买到的到底是什么?付出的三项代价是什么?
  3. 读己之写解决的是哪类用户投诉?举一个实现办法。
  4. 为什么"最后写赢"依赖时钟,而时钟在分布式下不可靠?
  5. Saga 与 TCC 的关键差别在哪两点上?各自适合什么场景?
  6. 去重标记为什么必须与业务操作在同一个事务里提交?
S18-B05 · 一致性的基本模型:强一致、最终一致与补偿
https://www.clxhxhhr.top/posts/6008/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。