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 = 每个请求都收到非错误响应)之间选一个。
“三选二”是错误的心智模型:
- P 不可选——网络分区是物理事实,不是设计选择。放弃 P = 只有一台机器 = 不是分布式
- CAP 只讨论分区发生时的极端情况,大部分时间网络正常,CAP 什么都没说
- C 和 A 不是二元的——实际系统在中间地带权衡
- CAP 没考虑延迟
PACELC 更实用
如果有 Partition → 选 A 还是 C?
Else(正常时)→ 选 L(atency) 还是 C(onsistency)?
| 系统 | 分区时 | 正常时 |
|---|---|---|
| PostgreSQL(同步复制) | PC | EC |
| PostgreSQL(异步复制) | PA | EL |
| etcd / ZooKeeper | PC | EC |
| DynamoDB | PA | EL(默认)/ EC(强一致读) |
| Cassandra | PA | EL(可调到 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)。
共识算法
共识是实现强一致性的基础。核心问题:多个节点如何就某个值达成一致,即使部分节点故障?
| 维度 | Paxos | Raft | ZAB |
|---|---|---|---|
| 设计目标 | 理论完备 | 可理解性 | ZooKeeper 专用 |
| Leader | 非必需 | 必需(强 Leader) | 必需 |
| 日志空洞 | 允许 | 不允许(连续提交) | 不允许 |
| 实现难度 | 极高 | 中等 | 中高 |
| 使用系统 | Chubby, 早期 Spanner | etcd, CockroachDB, TiKV | ZooKeeper |
| 容错 | 2f+1 节点容忍 f 故障 | 同左 | 同左 |
Raft 三个子问题:Leader Election(随机超时选举)+ Log Replication(Leader → 多数派确认 → 提交)+ Safety(新 Leader 必须包含所有已提交日志)。
实际系统的一致性选择
| 系统 | 单机 | 复制一致性 | 线性一致读 | 关键机制 |
|---|---|---|---|---|
| PostgreSQL | 强一致 | 同步=强一致,异步=最终一致 | 同步复制或读主库 | synchronous_commit 五档可调(off→remote_apply) |
| Redis | 强一致(单线程) | 最终一致(异步复制) | 仅读 Master | failover 可能丢数据 |
| Kafka | N/A | acks=all+min.insync=强一致 | N/A | ISR 机制:落后副本被踢出 |
| etcd | 线性一致 | 线性一致(Raft) | 是(ReadIndex/LeaseRead) | 线性一致读需 Leader 确认仍为 Leader |
| DynamoDB | 强一致 | 默认最终一致 | 是(强一致读选项) | 每分区 3 副本 |
| Cassandra | 最终一致 | 可调(ONE→QUORUM→ALL) | QUORUM 读+写≈强一致 | Quorum: R+W>N |
关键洞察:
- Kafka 的
min.insync.replicas至关重要——如果 ISR 缩到只有 Leader,acks=all等同于acks=1。推荐:replication.factor=3, min.insync.replicas=2, acks=all - Quorum 不等于强一致:Cassandra QUORUM 读写只保证副本集有交集,但 LWW + 时钟偏移可能导致后写被覆盖。真正线性一致需要共识算法
分布式事务
| 方案 | 一致性 | 隔离性 | 业务侵入 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| 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 |
三条选型原则:
- 默认从最弱开始,有明确理由才升级
- 同一系统内不同数据/操作可以用不同一致性级别
- 看实际实现机制(ReadIndex、LeaseRead),不看宣传语
Pitfalls
- “最终一致 = 不一致也没关系” — 最终一致只承诺收敛,没有时间上界。用户下单后 5 秒查不到订单不可接受的话,就需要更强的保证。对策:明确业务能容忍的不一致窗口
- 异步复制当同步用 — 写主库后立即读从库,读到旧数据。对策:写后短期读主库 /
synchronous_commit=remote_apply - 脑裂 — 网络分区后两个 Leader 同时写,数据分叉。Raft/Paxos 不会脑裂(需要多数派),但 Redis Sentinel 在某些 timing 下可能。对策:quorum + fencing token
- 分布式锁的 GC 暂停问题 — Redis SETNX 获取锁后 GC 暂停导致锁过期,两个进程同时”持有”锁。对策:fencing token(锁服务返回单调递增 token,资源端拒绝旧 token)
- 混淆”读到旧值”和”数据丢失” — 复制延迟 5 秒内主库挂了,这 5 秒数据永久丢失,不是”最终能读到”。对策:区分一致性(可见性)和持久性(数据安全),用
synchronous_commit控制 - Quorum 不等于强一致 — Cassandra QUORUM 读写只保证副本集交集,LWW + 时钟偏移可能覆盖后写数据。对策:真正线性一致需要共识算法
- 忽视跨数据中心延迟 — 选择线性一致但副本跨 Region,每次写入等 200ms 跨洋确认。对策:同 Region 内用强一致,跨 Region 用异步复制 + 最终一致