3156 字
约 10 分钟
18
S17-A04 · 分布式一致性选择:从强一致到最终一致
一致性不是"越好越贵"的单选题,而是一组按业务错误代价分配的决策。同一系统里,下单扣库存必须是强一致,商品浏览量统计可以差几分钟。判断的起点不是技术偏好,而是三个业务问题:错了要赔多少、能不能撤回、用户能不能等。下面按"层级 → 决策依据 → 实现代价 → 收敛与对账"的顺序给出结构。
1. 一致性谱系的四个层级
| 层级 | 保证 | 实际系统里的对应物 |
|---|---|---|
| 线性一致 | 所有操作像发生在单点上,且与真实时间顺序一致 | 单机数据库事务、共识算法日志(Raft/Paxos 提交后读主) |
| 顺序一致 | 所有节点看到同一顺序,但不保证与真实时间一致 | 单主复制、有序日志消费 |
| 因果一致 | 有因果关系的操作顺序不乱 | 会话粘滞 + 版本号、部分 CRDT 实现 |
| 最终一致 | 停止写入后,副本最终收敛 | 异步复制、消息投递、缓存回填 |
常见误读:"用了某中间件就得到了某层级"。
中间件提供的是机制(复制、日志、版本号),
实际层级取决于部署方式与读写路径选择。
例如同一套消息队列,读主 + 单分区 = 顺序一致;
多分区并行消费 = 只能靠业务做因果排序。
2. CAP 的常见误读与分区时的具体决策
| 误读 | 实际情况 |
|---|---|
| "三选二" | 分区不是可选项,P 迟早发生,真正的选择只在 C 与 A 之间 |
| "选了 CA 就没有 P" | 单机也有网络分区(与自身存储的连通),只是概率低 |
| "最终一致就是放弃 C" | 最终一致是延迟容忍,不是放弃正确性 |
分区发生时的决策模板(按数据类型分别填):
类型 1:账户余额、库存扣减 → 拒绝写入,保一致
类型 2:订单状态流转 → 主分区可写,少数侧只读
类型 3:浏览计数、推荐埋点 → 本地可写,异步合并
这张表要在设计阶段写完,而不是等分区发生时才讨论。多数"分区时数据错乱"的根因是从来没有按数据类型分过类。
3. BASE 与最终一致的工程含义
"最终一致"必须被量化,否则等于没有承诺:
不一致窗口 = 从写入成功到所有读路径可见的最长时间
必须明确三件事:
1. 窗口上限(例:P99 不超过 X 秒)
2. 窗口内的读行为(返回旧值?报错?排队?)
3. 窗口超限时的动作(告警、降级、进入对账)
| 概念 | 工程含义 | 落地物 |
|---|---|---|
| Basically Available | 允许返回旧值或部分数据 | 读路径降级规则 |
| Soft state | 中间态可被后续事件修正 | 状态机 + 补偿任务 |
| Eventually consistent | 存在可度量的收敛时间 | 对账与延迟监控 |
没有第三条的落地物,"最终一致"只是把问题推给运维。
4. 业务判断依据
| 判断维度 | 偏强一致 | 偏最终一致 |
|---|---|---|
| 错误代价 | 资金、库存、配额 | 展示、统计、推荐 |
| 可逆性 | 不可逆(已扣款) | 可重算(报表可重建) |
| 用户容忍度 | 秒级不一致即投诉 | 分钟级无感 |
| 冲突概率 | 高并发同资源竞争 | 不同用户互不影响 |
| 修复成本 | 人工介入、赔付 | 后台重跑 |
优先级:可逆性 > 用户容忍度 > 错误代价 > 技术偏好。
不可逆且高错误代价的数据,无论实现多麻烦都走强一致;
可重算的数据,追求强一致是纯浪费。
5. 强一致的实现代价
| 手段 | 代价 | 代价出现在哪 |
|---|---|---|
| 共识算法(Raft/Paxos) | 至少一次多数派往返的延迟 | 写延迟随节点数与地理分布上升 |
| 跨服务事务 | 协调者故障、锁持有时间、死锁 | 吞吐与可用性 |
| 单点写入 | 写入能力上限、故障切换期不可写 | 扩容与运维窗口 |
| 加锁/序列化 | 并发度下降 | 高峰期的排队 |
强一致的隐性成本清单:
- 变更为两阶段(准备 + 提交),回滚路径必须显式实现
- 参与方的超时与重试语义必须一致
- 监控需要新增"悬挂事务/未决事务"指标
6. 最终一致的四种收敛手段
| 手段 | 机制 | 适用 | 风险 |
|---|---|---|---|
| 重试 | 失败后指数退避重发 | 瞬时故障、网络抖动 | 重试风暴、重复执行 |
| 对账 | 周期性比对两侧数据 | 无法靠事件保证的路径 | 发现晚、修复动作需人工定义 |
| 补偿 | 执行反向业务操作 | 已产生的错误副作用 | 补偿本身失败、需幂等 |
| 事件重放 | 重放日志重建状态 | 计算逻辑变更、数据修复 | 需要事件不可变且可回放 |
-- 事务发件箱:业务写入与事件写入同一本地事务,保证不丢
BEGIN;
INSERT INTO orders(id, status, version) VALUES ('O1', 'CREATED', 1);
INSERT INTO outbox(id, aggregate_id, type, payload)
VALUES ('E1', 'O1', 'OrderCreated', '{"orderId":"O1"}');
COMMIT;
发件箱把"跨服务一致"降级为"本地事务 + 至少一次投递",这是最常用也最稳的一条:代价挪到了消费侧的幂等要求上。
7. 幂等与去重是一切的基础
CREATE TABLE idempotency_key (
key_hash CHAR(64) PRIMARY KEY,
biz_id VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL
);
-- 消费侧:先插入唯一键,冲突即视为重复,直接返回上次结果
INSERT INTO idempotency_key VALUES (?, ?, NOW());
| 机制 | 适用场景 | 局限 |
|---|---|---|
| 唯一键 | 创建类操作 | 需要稳定的业务唯一标识 |
| 版本号 | 更新类操作(乐观并发) | 需要读改写;冲突要重试 |
| 状态机 | 流程类操作 | 需要状态流转定义完整 |
| 结果缓存 | 查询类重试 | 结果体积与有效期 |
状态机口径:每个状态只允许特定入边,非法流转直接拒绝并记录。
"已支付 → 待支付"这类回退,只能通过显式退款动作,不能靠重试产生。
8. 顺序问题的来源与最小化
| 来源 | 现象 | 处理 |
|---|---|---|
| 多生产者 | 同一实体的事件交错 | 按实体 ID 分区/取模 |
| 分区并行 | 全局顺序丢失 | 只保留实体级顺序 |
| 重试 | 旧事件晚到覆盖新事件 | 版本号比较后丢弃 |
| 消费并发 | 同分区内并发处理 | 单分区单消费者串行 |
顺序性需求最小化:能只保实体级顺序,就不要求全局顺序。
全局顺序的代价是单点吞吐上限,几乎总是过度设计。
9. 读己之写的会话保证
跨服务下"用户刚提交就能看到"需要显式传递信号:
三种做法(按可靠性递增):
1. 写后短窗口内的读请求强制走主库/同分区
2. 携带写入版本号,读路径要求版本 >= 写入返回版本
3. 服务端会话粘滞(同一用户固定路由到同一副本)
| 做法 | 成本 | 失效场景 |
|---|---|---|
| 强制读主 | 主库压力上升 | 无 |
| 版本号约束 | 需要在读路径传递上下文 | 客户端不传版本时退化 |
| 会话粘滞 | 负载均衡复杂度上升 | 路由变更、故障切换后失效 |
10. 冲突解决策略
| 策略 | 机制 | 风险 |
|---|---|---|
| 最后写入胜出(LWW) | 比时间戳取最新 | 依赖时钟,会静默丢数据 |
| 向量时钟 | 记录因果关系,检测并发写 | 元数据膨胀、需要业务合并 |
| 业务合并 | 按业务规则合并(如集合求并) | 规则要能覆盖全部字段 |
| 单写者 | 指定唯一写入方 | 单点与切换成本 |
LWW 最危险的地方不是精度,而是它看起来总是"成功":没有任何报错,只是某次写入无声消失。凡是丢失不可接受的数据,都不要用时间戳决胜。
11. 对账系统的设计
| 要素 | 设计要点 |
|---|---|
| 周期 | 关键数据小时级,统计类天级 |
| 差异分类 | 缺失、多出、字段不一致、金额不一致 |
| 修复动作 | 补单、撤销、标记待人工;每类动作必须幂等 |
| 审计 | 每次差异与修复留痕,可追溯到源事件 |
-- 差异分类:以对账基准表为左表,全外连接找两侧不一致
SELECT a.id, b.id,
CASE WHEN b.id IS NULL THEN 'MISSING'
WHEN a.amount <> b.amount THEN 'AMOUNT_DIFF'
ELSE 'OK' END AS diff_type
FROM ledger a FULL OUTER JOIN cps_account b ON a.id = b.id
WHERE a.amount <> b.amount OR b.id IS NULL;
对账不是失败兜底,而是最终一致系统的必要组件:
没有对账的最终一致,等于把"最终"两个字交给运气。
12. 排错:六种一致性异常
| 现象 | 原因 | 处理 |
|---|---|---|
| 用户反复刷新才看到更新 | 读路径未保会话一致 | 加版本号约束或短窗读主 |
| 同一订单出现两次 | 消费侧无幂等键 | 引入唯一键或状态机 |
| 状态回退 | 旧事件晚到未被丢弃 | 版本号比较后丢弃 |
| 金额对不上 | 部分路径绕过对账 | 扩大对账覆盖,分类差异 |
| 高峰吞吐骤降 | 锁持有时间过长 | 缩短事务,改为发件箱异步 |
| 补偿越补越乱 | 补偿不幂等、可重复执行 | 补偿加唯一键,限重试次数 |
13. 边界:这些情况不要引入分布式一致
| 场景 | 判断 | 理由 |
|---|---|---|
| 单库事务能解决 | 用本地事务 | 分布式事务只增加故障面 |
| 能异步化 | 异步 + 最终一致 | 同步一致的延迟成本换不到业务价值 |
| 数据可重算 | 重算 + 对账 | 不需要事务保证 |
| 强一致需求只在单实体 | 单实体单写 + 版本号 | 不需要共识算法 |
| 一致性需求说不清 | 先量化不一致窗口 | 没量化就等于拍脑袋 |
判断顺序固定为:先问能不能不跨服务,再问能不能异步,最后才考虑分布式事务。多数被"分布式一致性"困住的系统,问题出在第一问没问。
14. 自测
- 线性一致与顺序一致的差别,用一句话怎么区分?
- 为什么说 CAP 里的 P 不是可选项,真正的选择只在 C 与 A 之间?
- 定义"不一致窗口"必须明确哪三件事?
- 四种收敛手段中,哪一种的修复动作最需要事先定义?
- 发件箱方案把一致性代价挪到了哪里?
- 为什么 LWW 比向量时钟更危险,尽管它实现更简单?
S17-A04 · 分布式一致性选择:从强一致到最终一致
https://www.clxhxhhr.top/posts/6075/ 评论
0 条
还没有评论,先写一条吧。