跳转到正文
zeno's blog
返回

分布式基础(一):一致性从强一致到最终一致的权衡光谱

专题: 分布式基础

Table of contents

Open Table of contents

TL;DR

“一致性”在 ACID/CAP/分布式模型中是三个完全不同的概念——ACID 的 C 是数据合法性,CAP 的 C 是线性一致性,分布式一致性模型是客户端可见性保证。选型的核心问题不是”要不要一致性”,而是”业务能容忍多大程度的不一致”。默认从最弱开始,有明确理由才升级。


三种”一致性”

语境含义关注点
ACID 的 C事务将 DB 从一个合法状态转移到另一个合法状态数据合法性(约束满足)
CAP 的 C线性一致性——所有节点同一时刻看到相同数据副本之间的一致
一致性模型定义读操作能看到哪些写操作的结果操作的顺序保证

ACID 的 C 是应用逻辑正确性,与分布式无关。后文”一致性”全指分布式语境。

CAP 定理

准确含义:在网络分区(P)发生时,必须在一致性(C = 线性一致性)和可用性(A = 每个请求都收到非错误响应)之间选一个。

“三选二”是错误的心智模型

  1. P 不可选——网络分区是物理事实,不是设计选择。放弃 P = 只有一台机器 = 不是分布式
  2. CAP 只讨论分区发生时的极端情况,大部分时间网络正常,CAP 什么都没说
  3. C 和 A 不是二元的——实际系统在中间地带权衡
  4. CAP 没考虑延迟

PACELC 更实用

如果有 Partition → 选 A 还是 C?
Else(正常时)→ 选 L(atency) 还是 C(onsistency)?
系统分区时正常时
PostgreSQL(同步复制)PCEC
PostgreSQL(异步复制)PAEL
etcd / ZooKeeperPCEC
DynamoDBPAEL(默认)/ EC(强一致读)
CassandraPAEL(可调到 EC)

一致性模型光谱

从强到弱。越强越容易编程,但性能越差、可用性越低。

模型保证不保证代价典型系统
线性一致性写完后所有读立即可见;全局实时顺序延迟最高,分区时不可用etcd, ZooKeeper(sync), Spanner
顺序一致性全局总序存在;进程内顺序保留实时性比线性一致稍低ZooKeeper(默认读)
因果一致性因果链保留(A→B 则所有人先看 A 再看 B)并发操作顺序需追踪因果关系MongoDB(causal session)
Read-your-writes自己写的自己能读到其他进程能否看到低(session affinity)大多数 DB session 级别
单调读时间不回退能读到最新值低(路由同一副本)大多数 DB 可配置
最终一致性最终收敛收敛时间、中间状态顺序最低延迟、最高可用DNS, DynamoDB, Cassandra

线性一致性 vs 顺序一致性

两者都要求全局总序,区别在于实时性。线性一致性尊重物理时间(A 先完成则全局序中 A 在 B 前);顺序一致性不要求与物理时间一致。

因果一致性:工程中的甜蜜点

因果一致性是在不牺牲可用性的前提下能达到的最强一致性(CALM 定理)。因果关系三种来源:进程内顺序、读-写依赖、传递性。

最终一致性的真实含义

只承诺收敛,不承诺时间上界,不保证中间状态的任何行为。冲突解决策略是核心设计决策(LWW、向量时钟、CRDT)。

共识算法

共识是实现强一致性的基础。核心问题:多个节点如何就某个值达成一致,即使部分节点故障?

维度PaxosRaftZAB
设计目标理论完备可理解性ZooKeeper 专用
Leader非必需必需(强 Leader)必需
日志空洞允许不允许(连续提交)不允许
实现难度极高中等中高
使用系统Chubby, 早期 Spanneretcd, CockroachDB, TiKVZooKeeper
容错2f+1 节点容忍 f 故障同左同左

Raft 三个子问题:Leader Election(随机超时选举)+ Log Replication(Leader → 多数派确认 → 提交)+ Safety(新 Leader 必须包含所有已提交日志)。

实际系统的一致性选择

系统单机复制一致性线性一致读关键机制
PostgreSQL强一致同步=强一致,异步=最终一致同步复制或读主库synchronous_commit 五档可调(off→remote_apply)
Redis强一致(单线程)最终一致(异步复制)仅读 Masterfailover 可能丢数据
KafkaN/Aacks=all+min.insync=强一致N/AISR 机制:落后副本被踢出
etcd线性一致线性一致(Raft)是(ReadIndex/LeaseRead)线性一致读需 Leader 确认仍为 Leader
DynamoDB强一致默认最终一致是(强一致读选项)每分区 3 副本
Cassandra最终一致可调(ONE→QUORUM→ALL)QUORUM 读+写≈强一致Quorum: R+W>N

关键洞察

分布式事务

方案一致性隔离性业务侵入性能适用场景
2PC强一致有(锁)差(阻塞)同数据中心、短事务
Saga最终一致长流程(电商、旅行)
Outbox最终一致N/A”写 DB+发消息”的原子性
TCC最终一致(有预留)高(三接口)需隔离的金融场景

Outbox Pattern:同一事务写业务数据 + outbox 表 → CDC/轮询发送到消息队列。利用 DB 事务原子性保证不丢消息。Debezium(CDC)比轮询更实时、压力更小。

TCC vs Saga:TCC 在 Try 阶段预留不执行(隔离性好),Saga 先执行再补偿(中间状态对外可见)。TCC 业务侵入极强(每个参与者三接口 + 幂等)。

一致性选型决策

不一致会导致资金损失/安全问题?
  YES → 强一致(单主 DB + 同步复制 / etcd)

用户能感知到不一致?
  YES 且不可接受 → Read-your-writes + 单调读
  YES 但可接受短暂延迟 → 最终一致性(有上界)

用户完全感知不到?
  → 最终一致性
业务场景推荐一致性实现方式
支付/转账强一致单主 DB 事务;跨服务用 2PC/TCC
库存扣减强一致数据库行锁 + 原子 SQL
用户注册/登录Read-your-writes写后短期读主库
IM 消息因果一致性逻辑时钟保证因果序
社交 Feed最终一致 + 单调读异步复制 + 客户端缓存
搜索索引最终一致异步事件→索引重建
配置管理/服务发现线性一致etcd / ZooKeeper

三条选型原则

  1. 默认从最弱开始,有明确理由才升级
  2. 同一系统内不同数据/操作可以用不同一致性级别
  3. 看实际实现机制(ReadIndex、LeaseRead),不看宣传语

Pitfalls

  1. “最终一致 = 不一致也没关系” — 最终一致只承诺收敛,没有时间上界。用户下单后 5 秒查不到订单不可接受的话,就需要更强的保证。对策:明确业务能容忍的不一致窗口
  2. 异步复制当同步用 — 写主库后立即读从库,读到旧数据。对策:写后短期读主库 / synchronous_commit=remote_apply
  3. 脑裂 — 网络分区后两个 Leader 同时写,数据分叉。Raft/Paxos 不会脑裂(需要多数派),但 Redis Sentinel 在某些 timing 下可能。对策:quorum + fencing token
  4. 分布式锁的 GC 暂停问题 — Redis SETNX 获取锁后 GC 暂停导致锁过期,两个进程同时”持有”锁。对策:fencing token(锁服务返回单调递增 token,资源端拒绝旧 token)
  5. 混淆”读到旧值”和”数据丢失” — 复制延迟 5 秒内主库挂了,这 5 秒数据永久丢失,不是”最终能读到”。对策:区分一致性(可见性)和持久性(数据安全),用 synchronous_commit 控制
  6. Quorum 不等于强一致 — Cassandra QUORUM 读写只保证副本集交集,LWW + 时钟偏移可能覆盖后写数据。对策:真正线性一致需要共识算法
  7. 忽视跨数据中心延迟 — 选择线性一致但副本跨 Region,每次写入等 200ms 跨洋确认。对策:同 Region 内用强一致,跨 Region 用异步复制 + 最终一致

分享这篇文章:

上一篇
tower(三):内置中间件-限流、重试、超时与缓冲
下一篇
tower(二):Layer 与 ServiceBuilder-中间件组合机制