3246 字
约 10 分钟
22
S17-A07 · 一致性模型选择:从业务不变量出发

"用了最终一致,用户刚改完就看不到自己的修改"——根因几乎从不在模型本身,而在建模顺序:先选了模型,再想业务能不能容忍。顺序该反过来:先写业务不变量,再问哪个模型守得住,以及守它要付什么钱。


1. 先把不变量写下来,模型才有意义

业务不变量 需要的最低强度 常见实现手段
余额不能为负、不能多扣 线性一致(单点裁决) 单库事务、条件更新 where balance >= x
库存不能超卖 线性一致或分段配额 行锁、Redis 原子扣减 + 对账
同一用户不能重复下单 幂等键 + 唯一约束 唯一索引、去重表、请求指纹
同一订单状态不能回退 单调写(版本号/状态机) 乐观锁 version、状态迁移表
用户看到自己的修改 读己之写 会话粘滞、版本令牌、主库读
列表页看到"差不多新"的数据 最终一致 缓存 + TTL、异步索引

判据:不变量要能一句话说清"什么情况算违反",且可检测。说不清的,是愿望不是不变量。

2. 五种强度的实际含义与代价递增

模型 实际保证 实现代价 典型场景
强一致(严格可串行化) 事务效果等价于某个串行顺序 最高:锁/冲突回滚,吞吐随争用骤降 单库资金、库存扣减
线性一致 所有操作看起来在单一时间点上原子生效 高:需要共识或单点裁决,跨机房要付 RTT 分布式锁、选主、配置中心
顺序一致 所有节点看到相同的操作顺序,但不保证实时 中高:单写者 + 全序日志 事件溯源、变更日志
因果一致 有因果关系的操作保序,无关操作可乱序 中:需要版本向量或因果令牌 协作编辑、评论回复
最终一致 停止写入后收敛,无时间界 低:异步复制,可能长时间不一致 计数、搜索索引、推荐特征

代价是单调递增的,但收益不是。从最终一致提到因果一致,往往能消掉大部分"看起来乱序"的用户抱怨;从因果再提到线性,成本会跳一个量级,收益却只落在少数关键路径上。所以正确的做法是按不变量分段,而不是给整个系统选一个模型。

3. 三个"便宜的补丁"能解决多少问题

大多数"最终一致不好用"的抱怨,其实只需要读己之写。

补丁 解决什么 成本 失效场景
读己之写 用户看不到自己的修改 低:会话粘滞或版本令牌 多设备、跨账号协作
单调读 刷新后看到旧数据(回退) 低:按版本选副本 副本故障切换瞬间
会话一致 同会话内操作保序 低:会话固定路由 会话跨节点漂移
// 读己之写:写完带版本号返回,读时要求副本版本 >= 该值
long v = writeService.update(cmd);          // 返回提交版本
response.setHeader("X-Read-Version", v);
// 读侧:副本版本不足则回源主库或等待追平(有上限)
return readService.read(key, v, Duration.ofMillis(200));

三个补丁能覆盖"改完看不到""刷新变旧""自己操作乱序"三类抱怨,成本只是一层路由加一次版本比较。先上补丁,再决定要不要共识:跳过这一步直接上强一致,最后往往把延迟问题归因于"分布式"。

4. 共识的真实成本

成本项 具体表现 工程后果
RTT 下限 一次写要等多数的确认,最少一个往返 跨机房写延迟由物理距离决定,优化空间很小
可用性牺牲 多数派不可用时拒绝写 少数派机房只能读,或整体降级
运维复杂度 成员变更、快照、日志压缩、脑裂恢复 需要专人值班与演练
吞吐上限 写串行化在 leader 上 热点 key 会把整个集群拖慢

判断:共识适合低频、高风险、必须唯一裁决的写(选主、配额、锁、配置),不适合高频业务写。把订单主流程塞进共识路径,等于用延迟换一个业务并不需要的保证。

5. CAP 与 BASE 常被误用的地方

误用说法 问题 更准确的表述
"三选二" 分区不是可选项,是必然发生的 分区期间在 C 与 A 之间选;无分区时两者都可要
"我们选 AP" 没说明哪些操作可以不一致 按操作分类:哪些必须拒绝写,哪些可以降级
"最终一致就够了" 缺少收敛时间界与对账 必须定义"多久算不一致"和发现后怎么修
"BASE 就是不管一致性" 把工程责任当成模型属性 BASE 是"基本可用 + 软状态 + 最终一致",软状态要有人收敛

CAP 的实用版本是:分区发生时,这个接口是拒绝写(保一致)还是接受写但可能冲突(保可用)。这个选择必须逐接口做,不能全系统一句话。

6. 补偿与对账:最终一致的收尾工作

最终一致不是"发个消息就完事",它必须配一套收敛机制:补偿(发现失败后反向操作)+ 对账(周期性比对两侧状态)。

-- 对账:找出超过阈值仍未收敛的差异
SELECT o.id, o.status, p.status
FROM orders o JOIN payments p ON p.order_id = o.id
WHERE o.status <> p.status
  AND o.updated_at < NOW() - INTERVAL 10 MINUTE;
机制 触发方式 适用 风险
即时补偿 消费失败/超时 短链路、可逆操作 补偿本身失败会放大问题
定时对账 周期扫描差异 资金、库存、订单 扫描成本高,需分批限流
人工工单 对账差异转工单 低频高价值 处理时效依赖人

判断标准很朴素:如果这条链路出问题时没人能在一小时内发现,那它就不该用最终一致。

7. 至少一次投递,意味着你必须幂等

消息系统给的通常是"至少一次",重复是常态而不是异常。幂等的实现不是"业务判断一下",而是一张去重表加一个唯一约束:

INSERT INTO msg_dedup(msg_id, biz_key, created_at)
VALUES (?, ?, NOW());        -- 唯一索引冲突 = 已处理,直接返回成功
try {
    dedupDao.insert(msgId, bizKey);   // 唯一键 (msg_id) 或 (biz_key)
} catch (DuplicateKeyException e) {
    return Ack.OK;                    // 幂等返回,不重复执行业务
}

幂等键的选择比实现更重要:用 msg_id 只能挡住同一消息的重投,挡不住"上游重发了一条新消息";用业务键(订单号+操作类型)才能挡住业务级重复。两者叠加最稳。

8. 跨服务事务的四条路与失败模式

方案 一致性强度 失败模式 适用
本地消息表 最终一致 消息表与业务表同库,跨库仍要两段 单库业务 + 异步下游
事务性发件箱(outbox) 最终一致 投递重复、顺序错乱 需要可靠投递的事件驱动
Saga 最终一致(可补偿) 补偿失败、中间态可见、缺隔离 长流程、跨多个服务
TCC 接近强一致(业务层) Try 悬挂、空回滚、Confirm 重试 资金、库存等强约束

发件箱的最小实现是把"改业务"与"登记事件"放进同一个本地事务:

BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = ? AND status = 'PENDING';
INSERT INTO outbox(id, topic, payload, status) VALUES (?, 'order.paid', ?, 'NEW');
COMMIT;
-- 独立投递进程按 status='NEW' 扫描并发送,成功后置 SENT

判断顺序:能单库事务就别拆;拆了先看能不能用本地消息表;需要长流程补偿才上 Saga;TCC 只在"中间态不能对外可见"时才有必要。 TCC 的三个坑(悬挂、空回滚、幂等)不是实现细节,是这种模式的固有成本。

9. 可见性延迟怎么向用户表达

技术选择最终要落到文案上。同一件事,说法不同,容忍度差很多:

场景 差的表达 好的表达
异步索引,搜索延迟 搜索结果可能不是最新 刚发布的内容约 10 秒后可被搜索到
跨机房同步 数据同步中 已保存,其他地域稍后可见
异步对账 处理中 预计 2 分钟内完成,完成后通知你

原则:给时间界,不给"可能"。用户能接受"10 秒",不能接受"不确定"。给不出时间界,说明收敛时间从未被测量——那是工程问题,不是文案问题。

10. 怎么测一致性

手段 验证什么 做法
故障注入 分区/节点挂掉时的行为 断网、停副本、延迟注入,观察是否拒绝写而非错写
延迟注入 乱序与重复的容忍度 给消息加随机延迟与重复投递
并发对账 不变量是否被破坏 高并发压测后跑对账 SQL,差异必须为 0
线性一致检查 是否出现"读到旧值再读到新值" 记录读写历史,检查可否线性化排序

最有价值的是第三项:不变量检查比模型名称可靠。自称强一致没用,并发一千次后余额对不上就是不对。

11. 排错

现象 根因 处理方向
刚写完读不到 读走副本、无读己之写 加版本令牌或会话粘滞
刷新后数据回退 单调读缺失、副本间延迟不一 按版本选副本,禁止回退读
偶发重复扣款 非幂等消费 + 至少一次投递 幂等键 + 唯一索引
库存超卖 先查后改,非原子 条件更新或原子扣减
Saga 卡在中间态 补偿失败无重试 补偿也要幂等 + 重试 + 人工兜底
跨机房写延迟高 把共识放进了热路径 拆出关键裁决点,其余走本地

12. 边界

场景 判断
单库单实例 不必谈分布式一致性,事务足够
纯日志、埋点、推荐特征 弱到底,丢了也不影响业务
强一致要求且流量不高 不要拆服务,单库最省
只读展示、允许分钟级延迟 最终一致 + 缓存,别上共识
跨地域强一致写 先问业务是否真需要,通常答案是"局部需要"

一致性模型是一条按路径分段的选择题:登录、下单、支付、报表可以各自不同。给整个系统贴一个标签,是最常见、也最贵的建模错误。

13. 自测

  1. 为什么"先选模型再想业务"几乎必然出问题?
  2. 读己之写、单调读、会话一致,各自挡住哪一类用户抱怨?
  3. 共识的成本里,哪一项无法靠优化消除?
  4. CAP 在无分区时该怎么说才不失真?
  5. 幂等键用 msg_id 和用业务键,挡住的东西有什么不同?
  6. 为什么"不变量检查"比"模型名称"更可靠?
S17-A07 · 一致性模型选择:从业务不变量出发
https://www.clxhxhhr.top/posts/6158/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。