第一次遇到"数据不一致"通常是在对账时:订单显示已支付,账务没入账;库存减了,订单却超时取消。查日志每一步都成功,只是没人在同一个事务里。
单库时代数据库事务兜住了这件事,一旦拆库、加缓存、上消息队列,兜底的人就没了。这篇把一致性拆成三个可判断的问题:谁必须立刻看见这次修改,谁可以晚点看见,晚点的那部分谁负责纠正。
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. 自测
- "数据不一致"与"数据错了"有什么区别?两者分别该修什么?
- 强一致买到的到底是什么?付出的三项代价是什么?
- 读己之写解决的是哪类用户投诉?举一个实现办法。
- 为什么"最后写赢"依赖时钟,而时钟在分布式下不可靠?
- Saga 与 TCC 的关键差别在哪两点上?各自适合什么场景?
- 去重标记为什么必须与业务操作在同一个事务里提交?