第 34 章 分布式理论:CAP、共识与一致性
CAP 与一致性模型给出"能承诺什么",Raft 给出"怎么做到",分布式事务、ID、锁、限流与分片则是这套理论在工程里的六张落地账单。
学习目标
读完本章,你应当能够:
- 用 C/A/P 的准确定义破除"三选二"误读,说出分区发生时真正的二选一,并用 PACELC 给
Cassandra/HBase/MongoDB/MySQL主从定位; - 按强弱排出线性一致、顺序一致、因果一致、最终一致的谱系,并用具体执行历史区分线性一致与顺序一致;
- 独立推导 Raft 的选举超时随机化、
AppendEntries一致性检查、commitIndex推进规则,以及为什么不能直接提交旧任期的日志(Figure 8 式反例); - 说出 Paxos 两阶段的活锁成因与 Multi-Paxos 的改进点,并辨析 Raft 与 ZAB 的模型差异;
- 列出六种分布式事务方案各自的不一致窗口,与六种分布式 ID 方案的失效场景;
- 说清四种分布式锁各在什么时候不安全,以及
Redlock争议的结论与 fencing token 的位置; - 算出一致性哈希下取模扩容的键迁移比例,说明虚拟节点为什么能消除倾斜。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
Redis 的 SET NX PX、Lua 释放锁、看门狗与主从异步复制 | 见第 27 章 27.11 节;本章只做理论对比与缺陷分析 |
| 主从复制模式、复制延迟与脑裂 | 见第 26 章 26.1、26.2 节 |
| MQ 的可靠投递、重复消费与幂等原语 | 见第 33 章 33.2、33.4 节;接口幂等落地见第 35 章 |
| 事务 ACID、redo/undo、两阶段提交 | 见第 21 章 21.5 节、第 22 章 22.1 节 |
| 哈希函数、环形结构、树与多数派计数 | 数据结构课程内容;本章直接进机制 |
34.1 CAP 的准确表述与 PACELC
34.1.1 先把三个字母的定义读准
面试里九个候选人里九个会说"CAP 是三选二",这是本章第一个要拆掉的错误。CAP 出自 Brewer 的猜想与 Gilbert、Lynch 的证明,三个字母的准确含义是:
- C = 线性一致性(linearizability),不是"数据一致"。它要求整个系统对外表现得像只有一个副本、且每个操作在其调用与返回之间的某一瞬间原子生效。注意这是一个强到苛刻的模型:它同时约束了"所有节点看到同一顺序"和"这个顺序必须与真实时间兼容"。很多系统自称"强一致",实际只做到顺序一致或读己之写。
- A = 可用性,准确定义是"每一个非故障节点收到的每一个请求,都能在有限时间内返回一个非错误响应"。关键词是"非故障节点"与"非错误响应":超时不算可用、返回
503不算可用、返回可能过期的数据算可用。所以 A 不等于"高可用",它是有界响应性。 - P = 分区容忍,指网络被切成若干互不可达的子集时系统仍能继续运行。这里的"分区"是网络分区(network partition),不是数据分区(sharding)。
小心
把 A 理解成"高可用(99.99%)“是本条最危险的答法。可用性是"是否在有界时间内给出非错误响应”,而通常说的"高可用"是年度可用率指标。二者相关但不同:一个每次都在 1 ms 内返回 500 的系统可用性定义上不满足 A,但它可能很"少宕机"。面试官听到"高可用"就会追问"那 A 和 SLA 里的可用率是什么关系",答不上就崩。
34.1.2 “三选二"错在哪
“三选二"隐含一个前提:三者权重相同、可以任意舍弃一个。两个错误:
- P 不是可选项。只要系统跨网络部署,交换机故障、GC 长暂停、网线被拔都会产生分区。分区不是"要不要"的问题,而是"什么时候发生"的问题。既然 P 必须保留,“三选二"就退化成二选一,且只在分区期间。
- CA 系统在分布式语境下不存在。单机数据库满足 CA(没有分区可言),但一旦跨机,就必须面对分区。 所以正确表述是:在分布式系统中 P 是前提,真正的选择是"分区发生时,宁可拒绝服务以保证线性一致(CP),还是宁可返回可能陈旧或冲突的数据以保证可用(AP)”。而且这个选择只在分区窗口内生效——没有分区时,CAP 什么都不约束,这正是 PACELC 要补的那一半。
34.1.3 PACELC:没有分区时的另一半账
PACELC(读作 “pass-el-see”)由 Abadi 提出,把 CAP 补全为两段:if Partition then A else L, else C —— 即"分区时在 A 与 C 之间选;否则(Else)在延迟 L 与一致性 C 之间选"。
第二段才是日常态。以三副本为例:MongoDB 的多数派写(w:majority)要等两个副本确认,比 w:1 多出一个跨机房往返;单机房 RTT 约 0.1~0.5 ms,跨可用区往往按毫秒到几十毫秒计,跨大洲约 150 ms。同步复制用延迟买一致性,这就是 C 与 L 的交换。
| 系统 | 分区时的选择 | 无分区时的选择 | 类别 | 原因 |
|---|---|---|---|---|
Cassandra | 多数派可用即可读写 | 默认 ONE 级读写,可调 QUORUM | PA/EL | 无主多写,副本异步收敛,一致性级别由客户端逐次指定 |
HBase | 分区时 Region 不可用 | 强一致读写,WAL + 单 Region 单写者 | PC/EC | 依赖 HDFS,RegionServer 故障期间该 Region 阻塞 |
MongoDB | 主节点不可写,等选举 | 默认 w:1 时近 EL,w:majority 时 EC | PC/EC(可调) | 单主复制,写关注级别决定落在哪一档 |
MySQL 主从(异步) | 主库不可写,需切主 | 主库提交即返回,从库异步追 | PC/EL | 复制是异步的,读从库可能落后,延迟最低 |
ZooKeeper/etcd | 少数派侧拒绝服务 | 写走多数派,读可线性化 | PC/EC | 共识协议要求多数派,天然 CP |
注意
PACELC 把"系统属于哪一类"从静态标签变成了可调的旋钮。同一个 Cassandra 集群,ONE 级读是 EL,QUORUM 读就靠近 EC。面试时先说清"这个系统的默认配置落在哪一档”,再谈调参后怎么移动,比背一个标签更有说服力。
34.1.4 BASE 与柔性事务
BASE(Basically Available, Soft state, Eventually consistent)是 AP 路线的工程口号,与 ACID 相对。它的准确落点是放弃"事务边界的原子性由数据库保证”,把原子性移交应用层:允许中间态被外部观察(软状态)、允许读暂时陈旧(最终一致),用柔性事务(flexible transaction)换可用性与吞吐。柔性事务的共同特征是"没有全局锁或全局锁只持有极短时间,一致性由重试、补偿、对账三条路兜住",具体六种方案见 34.5 节。判断一个方案是刚是柔,只问一句:它有没有在一段时间内让某个资源全局不可用? 有就是刚性(如 2PC),没有就是柔性。
34.2 一致性模型谱系
一致性模型不是"程度的形容词",而是一条从强到弱、可用性和延迟逐级改善的谱系。判断一个历史是否合法,看它是否满足对应模型的约束。
| 模型 | 约束 | 反例(不满足的情形) | 典型代价 |
|---|---|---|---|
| 线性一致(linearizable) | 存在一个全序,且该全序与真实时间兼容;读读到的是"最近一次已完成写" | 写返回后立刻读仍读到旧值 | 每次写要多数派确认,跨机房 RTT 进入关键路径 |
| 顺序一致(sequential) | 所有节点看到同一个操作全序,且保持每个进程内部的程序顺序;但不要求与真实时间一致 | 两个节点对同一批操作的顺序不一致 | 需要一个全局定序者或共识,但读可以走本地 |
| 因果一致(causal) | 只对有因果关系的操作保序(写后读、读后写、传递闭包);并发操作可任意排序 | 同一进程先写后读,读却看不到自己的写 | 需要跟踪因果依赖(版本向量、依赖时钟) |
| 最终一致(eventual) | 停止写入后,经过有限时间所有副本收敛到同一值 | 一直有写入时任意陈旧 | 几乎零协调成本,冲突需应用层解决 |
线性一致与顺序一致的真实差别在"时间"上。 例子:P1 写 x=1 并在 t=10 返回;P2 在 t=20 发出读,t=21 返回 x=0;同时 P3 也读 x。顺序一致是可以允许这个历史的——只要所有节点一致地认为"P2、P3 的读排在 P1 的写之前"这个全序成立,就满足顺序一致(程序顺序没被违反,因为 P2 的读和 P1 的写来自不同进程)。但线性一致不允许:写在 t=10 已完成,读在 t=20 才开始,两者在真实时间上不重叠,读必须看到 x=1。 |
重要
一句话记牢:线性一致 = 顺序一致 + 真实时间约束。只满足"大家看到同一顺序"的寄存器(如某些顺序一致但非实时的共享内存)可以出现"写已完成,之后开始的读却读不到"的现象,这在顺序一致下合法、在线性一致下非法。这也是 ZooKeeper 默认读(可走 Follower 本地)只能给顺序一致、要线性化读必须显式 sync 的原因。
因果一致是工程上性价比最高的一档:它恰好挡住"评论先于原帖可见"这类反直觉现象,同时不要求全局协调。CAP 里的 A 与 C 之争,实际是"要不要线性一致"之争;如果只要求因果一致或最终一致,很多 AP 系统就能同时给出可用性与可接受的一致性。
[!TIP]
面试话术:被问"最终一致是不是就是很弱",答"它的强项是收敛性有界——停止写入后有界时间收敛,冲突用 LWW/版本向量/CRDT 解决;它不是’不保证’,而是’保证的东西更少但可用性更高’",然后主动给一个"同一商品在两个机房看到不同库存"的业务例子。
34.3 Raft:从选举到提交的完整推导
34.3.1 三种角色、任期与状态
Raft 把节点分为 Leader、Follower、Candidate 三种角色,并把时间切成递增的任期(term)(连续整数)。每个节点持久化 currentTerm 和 votedFor,term 的作用是给所有消息与日志条目打上单调的逻辑时间戳,让节点能一眼判断"这条消息是否过期"。
| |
每个节点还维护两个易失索引,这是 34.3.3 全部推导的基础:commitIndex(已知被多数派持久化的最高日志下标,可安全应用到状态机)与 lastApplied(已应用到状态机的最高下标);Leader 额外维护每个 Follower 的 nextIndex(下一条要发的下标)与 matchIndex(已确认复制到的最高下标)。
注意
commitIndex 与 lastApplied 分离是必须的:提交是"复制到位"的事实,应用是"执行到状态机"的动作,两者之间隔着一次批量应用。这也是 Raft 状态机可以做到"日志先落盘、状态机稍后追"的原因。
34.3.2 选举:随机化超时为什么是必需品
Follower 在选举超时内没收到任何 AppendEntries 或投票请求,就自增 currentTerm、投自己一票、转为 Candidate,向所有节点并发发 RequestVote。
RequestVote 的关键参数与投票规则:
- 参数:
term(候选人任期)、candidateId、lastLogIndex、lastLogTerm。 - 投票条件(两条都必须满足):
term不小于自己的currentTerm,且候选人的日志"至少和我一样新"。日志新旧的比较是:先比最后一条日志的term,大者更新;term相同再比lastLogIndex,大者更新。 - 拿到多数派(N/2 + 1)选票才当选;每个任期每个节点只投一票(
votedFor持久化,防止重启后重复投票)。 为什么要随机化超时?假设所有节点固定在同一个超时时间。它们会几乎同时超时、同时进入Candidate、同时把票投给自己。N个节点分成N份选票,没有任何人拿到多数派(这是"选票瓜分",split vote),然后所有人同时等待下一个同样的超时,再次同时开选——系统可以无限期无法选出 Leader。
| |
随机区间通常取 150~300 ms(Raft 论文口径);网络 RTT 越大、节点越多,区间要相应放宽,但要保证随机上限明显小于"业务可容忍的选举中断时间",否则每次换主都要停顿几百毫秒。
警告
心跳间隔必须远小于选举超时下限(论文建议心跳约 100 ms 量级、超时下限 150 ms 量级)。如果心跳间隔接近超时下限,网络抖动就会让 Follower 频繁误判 Leader 已死并发动无谓选举,term 疯涨、集群反复换主。这是"集群莫名反复选主"最常见的配置根因。
34.3.3 日志复制:两条索引与一次一致性检查
Leader 收到客户端请求后,先追加到本地日志,再向所有 Follower 并发发 AppendEntries,携带新条目以及两个"锚点":prevLogIndex(紧邻新条目之前的日志下标)与 prevLogTerm(该条目的任期)。
| |
Follower 的两条规则:prevLogIndex 处的条目必须存在且 term 等于 prevLogTerm,否则拒绝(并可直接带上自己冲突位置的提示以加速回退);接受后若新条目与已有条目冲突,删除从冲突点开始的全部后续条目再追加。
拒绝时 Leader 的处理是回退重试:把该 Follower 的 nextIndex 减一,重新发上一轮的 AppendEntries,直到找到双方一致的那个点。这是一次"二分式的逐条回退",最坏要退到 nextIndex=1。优化手段是让 Follower 在拒绝响应里直接回报冲突条目的 term 与该 term 的首个下标,Leader 一次跳过整个冲突任期,把 O(日志长度) 次往返降到 O(冲突任期数) 次。
提示
面试加分句:nextIndex 是乐观的 Leader 侧猜测(初值 lastLogIndex+1),matchIndex 是已被证实的复制进度(初值 0)。一条日志"在多数节点的 matchIndex 之上"才是可提交的候选;nextIndex 只用来决定下一次发什么。
34.3.4 commitIndex 的推进规则与"不能提交旧任期日志"
Leader 的提交规则有两条,缺一不可:
- 只有当前任期的日志条目才能通过"计数多数派"直接提交;
- 一旦当前任期的某条目被提交,它之前的所有条目(含旧任期的)也被连带提交——因为被提交的条目在所有日志中都位于更早的位置,Followers 的日志按 Raft 日志匹配性质与 Leader 一致。
为什么不能直接提交旧任期日志?用 Figure 8 式反例逐步推。设 5 个节点
S1~S5,多数派 = 3。
| |
此刻如果 Raft 允许"只要某条旧任期日志落在多数派上就提交它",则 idx2=term2 被提交,进而 idx2=term3(S5 那条)也被连带提交。但时刻③ 之后 S5 若再次当选并能复制日志,它会把 idx2=term2 覆盖掉——一条已提交的条目被覆盖,状态机安全性直接破裂。
根因是:旧任期条目"看起来在多数派上"这件事不代表它现在还安全,因为那条目所在位置的多数派可能已被后来的任期内选举换掉。反过来,只要当前任期的条目提交了,旧任期条目就必然安全:当前任期条目能过多数派,说明多数派的日志已经包含了它之前的所有条目(prevLogIndex/prevLogTerm 检查保证了这一点),而任何未来 Leader 都必须从这个多数派里产生,必然带有这些条目。所以规则 1 是安全性的充分条件,规则 2 是它的副产品。
小心
答"Leader 的日志只要复制到多数派就能提交"会被判错。正确答法是"当前任期的条目复制到多数派即可提交;旧任期条目不能靠计数提交,只能等当前任期条目提交后被连带提交"。这个点几乎必然被追问,也是区分"背过 Raft"和"想过 Raft"的分界线。
若集群长期没有新写入,commitIndex 就停住,旧条目迟迟不提交,读操作会受到什么影响,是下一层的追问(解:新 Leader 可在任期开始提交一条 no-op 条目来立刻推进 commitIndex)。
34.3.5 网络分区下的两个 Leader
5 节点被切成 {S1,S2} 与 {S3,S4,S5}:
- 多数派侧
{S3,S4,S5}:能选出 Leader、能提交日志、正常对外服务。 - 少数派侧
{S1,S2}:原 Leader 若在其中,它仍然认为自己是 Leader(它没有全局视野),但任何AppendEntries都拿不到多数派确认,所以commitIndex无法推进,一条日志都提交不了,客户端写入全部超时。 - 分区恢复:少数派侧 Leader 收到更大
term的心跳就退回Follower,其本地未提交的日志被多数派侧的日志覆盖回滚;lastApplied之后的条目要回退到提交点重新应用。 这个"双主"是良性的,前提正是 34.3.4 的规则:少数派侧的"主"没有提交能力。如果它也能提交,就会出现两个状态机各自前进并永久分叉。
34.3.6 成员变更:为什么不能一次换掉一半
集群配置(membership)本身也是一份需要共识的数据。如果直接把 {A,B,C} 改成 {A,B,C,D,E} 并让两边各自独立选主,可能出现 {A,B,C} 选出 L1、{A,B,C,D,E} 选出 L2 的局面,两个多数派互不重叠,安全性破裂。
- 单节点变更(one server at a time):一次只增删一个节点。任意两步配置的多数派必然相交,因此不会出现两个不相交的多数派。实现简单,但是串行的,扩容 10 个节点要 10 轮。
- 联合共识(joint consensus):先切到
C_old,new(提交需要两个配置各自的多数派都同意),再切到C_new。任意时刻最多只有一个配置的多数派能独立工作。代价是实现复杂、需要额外的中间态处理。
注意
面试官会追问"加节点是扩容还是缩容更危险"。答案:加节点本身不提升写吞吐(写仍要多数派确认,节点越多单次写越慢),它提升的是读容量与容错度;节点数从 3 加到 5,容错从 1 台变 2 台,但每次写要多等一个副本。偶数节点没有收益(4 节点容错仍是 1 台,因为多数派是 3),所以共识集群通常是 3、5、7。
34.4 Paxos、ZAB 与 Raft 的关系
Paxos 解决的最小问题是"一组进程对单个值达成共识"(Basic Paxos),两阶段:
| |
两个必须点破的地方:其一,提案编号必须全局唯一且单调(用 <序号, proposerId> 保证);其二,Phase 2 的值不是提议人自由选的——若多数派中有节点已接受过某值,提议人必须采用"最高编号对应的那个值",这正是"已选定的值不会被推翻"的原因。
Basic Paxos 的活锁:P1 发 Prepare(1),P2 发 Prepare(2),各自在对方 Accept 前用更大编号抢占,然后互相不断加号——没有人能完成 Phase 2,谁也不违反安全性,但永远达不成决议。Multi-Paxos 的改进是:选出一个稳定的 Leader(distinguished proposer)由它串行提出后续所有 Proposal,并把 Phase 1 一次性对所有日志槽位做完(后续只跑 Phase 2),把每轮 2 个 RTT 降到稳态约 1 个 RTT。
Raft 的定位是为可理解性而设计的 Multi-Paxos 变体:它做了三件 Paxos 没规定的事——强 Leader(日志只能从 Leader 单向流向 Follower,Paxos 允许任何提议人)、连续的日志槽位与 prevLogIndex/prevLogTerm 检查(Paxos 的 slot 是独立的)、把成员变更写进协议。两者共享的核心约束完全相同:多数派交集保证任意两个已选定的决议不会冲突,编号/任期单调保证旧消息被识别并丢弃。
| 维度 | Paxos(Basic/Multi) | Raft | ZAB |
|---|---|---|---|
| 抽象目标 | 对单个值/单个槽位达成共识 | 复制日志(值序列) | 主备模型下复制状态变更(zab 事务) |
| Leader | Basic 无;Multi 有弱 Leader(distinguished proposer) | 强 Leader,日志单向 | 强 Leader,但语义是"主备"而非"领导选举" |
| 日志空洞 | 允许未选定的槽位存在 | 不允许,日志连续 | 不允许,zxid 连续 |
| 顺序标识 | Proposal 编号 n | term + logIndex | zxid(64 位:高 32 位 epoch + 低 32 位计数器) |
| 成员变更 | 未规定 | 单节点变更 / 联合共识 | 由 Leader 以特殊事务广播 |
| 设计目标 | 正确性优先,抽象度高 | 可理解性与可实现性 | 为 ZooKeeper 的主备高可用服务 |
一句话区分:ZAB 是"主备(primary-backup)复制协议",Raft 是"领导者选举协议"。 ZAB 假设有一个主并围绕它做状态复制与恢复(ZooKeeper 重启后要靠 zxid 做差异同步);Raft 把"谁来当主"当成一等公民,用 term 与选举超时这套机制统一处理换主与日志复制。答题时不要把两者说成"同一个东西的两种实现"。 |
34.5 分布式事务六种方案对比
| 方案 | 一致性模型 | 不一致窗口 | 阻塞/可用性 | 业务侵入 | 适用场景 |
|---|---|---|---|---|---|
2PC | 强(准线性) | 无脏数据,但有长时间锁定 | Coordinator 单点,prepare 后事务处于"不确定态",参与者持锁阻塞 | 低(数据库/中间件实现) | 单体内跨多个库、或对强一致要求高的短事务 |
3PC | 强 | 仍无法消除脑裂,但大幅缩短阻塞窗口 | 加 CanCommit 预询与超时自动 commit,参与者不再是"永久不确定" | 低 | 教科书方案,实践中因网络分区仍不安全而少用 |
TCC | 最终一致 | Try 成功到 Confirm 之间的业务中间态 | 无长事务锁,靠 Try 预留资源 | 高(每个操作写三段) | 资金、库存等需要预留与释放的场景 |
Saga | 最终一致 | 每个本地事务提交后即对外可见,中间态暴露 | 无全局锁,失败靠反向补偿 | 中(每步写补偿) | 长流程(订单→支付→发货→积分),步骤多、耗时长 |
| 本地消息表 | 最终一致 | 本地事务提交到消息被消费之间的窗口 | 与业务同库同事务,不引入新单点 | 中(多一张表与一个投递任务) | 已有单库、想用最小代价换取最终一致 |
| 最大努力通知 | 最终一致(弱) | 消息可能永久丢失(重试次数耗尽即放弃) | 不保证送达 | 低 | 对账、通知类等容忍丢失的场景 |
2PC 的真实问题不是"慢",而是"不确定态"。 协调者发出 prepare 后、发出 commit/rollback 前崩溃,参与者已锁定资源但不知道该怎么办;此时若协调者日志单点丢失,参与者只能阻塞等待人工介入。此外 prepare 后数据对同库其他事务是否可见取决于隔离与锁的粒度——InnoDB 里 prepare 只是把事务状态推进到 PREPARED 并持锁,其他事务读该行会被锁阻塞,所以"数据可见性"真正的坑在锁的持有期跨越了一次跨机往返(详见第 21 章 21.5 节)。3PC 增加 CanCommit 并把 preCommit 后参与者可超时自行 commit,缓解了阻塞,但超时自行 commit 在分区下会导致部分节点提交、部分回滚,所以它牺牲的是安全性,实测并没有真正解决问题。 | |||||
TCC 的三个坑必须在设计时写进注释。 Try 阶段做资源预留(冻结金额、锁定库存),Confirm 提交、Cancel 释放。 |
| |
TCC 三大坑:空回滚(Try 未到、Cancel 先到,不能报错也不能什么都不做,要落防悬挂标记)、幂等(Try/Confirm/Cancel 都可能重试)、悬挂(Cancel 先到并释放了资源,Try 随后才到并真的冻结,资源被永久占用——防悬挂标记就是为它存在的)。这三个坑在面试里几乎是 TCC 的必问项。
警告
答 Saga 时必须主动说"它不提供隔离性"。Saga 的每个本地事务提交后立刻对外可见,所以别的请求可能读到"订单已创建但尚未支付"的中间态;补偿事务也不一定能真正撤销已发生的副作用(已发出的短信无法撤回)。缓解手段只有"语义锁"(业务上把中间态标记为不可消费)或按业务键串行化,而不是靠数据库锁。
Seata 的三种模式定位:AT(Automatic Transaction)对业务零侵入,靠拦截 SQL 解析出前后镜像(before/after image)写入 undo_log,一阶段提交本地事务并立即释放本地锁,但持有全局锁(global lock)直到全局事务结束,二阶段若需要回滚就用 undo_log 反向补偿;TCC 模式把三段留给业务实现,性能与可控性最好;Saga 模式面向长流程、状态机编排。AT 的关键机制是全局锁:它保证同一行记录不会被两个全局事务同时修改(否则两个事务的 undo_log 会互相覆盖),代价是同一行的跨服务并发被串行化,热点行会成为瓶颈。此外 AT 要求本地事务提交前有可用的 undo_log,且能被二阶段读到——这是 AT 与"仅在本地库回滚"的本质差别。
[!NOTE]
面试官常追问"AT 的全局锁和数据库行锁有什么区别"。答:数据库行锁在一阶段结束就被释放(所以本地事务的隔离性只覆盖一阶段),全局锁跨两个阶段持有,是跨服务的逻辑锁,通常存在 Seata 服务端或数据库表中;它的作用不是保证隔离级别,而是保证二阶段补偿时数据没有被别人改过。
34.6 分布式 ID 的六种方案与取舍
| 方案 | 有序性 | 单机性能量级 | 可用性依赖 | 失效场景 |
|---|---|---|---|---|
UUID(v4) | 无序(v7 可时间有序) | 本地生成,10⁶/s 以上量级 | 无依赖 | 无序导致 B+ 树索引页分裂(见第 23 章 23.4 节);36 字符存储与可读性差 |
| 数据库自增 | 严格递增 | 受单库写入上限约束(千级/s 量级) | 数据库单点 | 单点故障即全站阻塞;多实例步长方案在扩容时要改步长 |
| 号段模式 | 趋势递增 | 一次取一批(如 1000 个),QPS 提升 10³ 倍量级 | 依赖中心存储(DB/Redis) | 中心存储宕机时只能靠本地缓存号段撑住;号段取完瞬间有 DB 尖峰 |
Snowflake | 趋势递增(同毫秒内有序) | 单机 4096 个/ms,4096 × 1000 = 409.6 万/s 上限 | 无依赖(靠机器位) | 时钟回拨导致 ID 重复;机器位分配冲突 |
Redis INCR | 严格递增 | 10⁵/s 量级(单实例) | 依赖 Redis 可用性 | Redis 不可用即发不出号;持久化策略不当重启会重号 |
Leaf/UidGenerator | 各自可选 | Leaf-segment 走号段,Leaf-snowflake 走雪花 | 号段依赖 DB,雪花依赖 ZK 校验 | 见下 |
Snowflake 的 64 位结构(Java long,最高位留给符号): |
| |
10 bit 常拆成 5 bit 数据中心 + 5 bit 机器(各 32)。4096 × 1000 = 4,096,000/s,这是单机理论上限。
时钟回拨是 Snowflake 唯一的致命伤,因为算法假设"时间戳单调不减",回拨后同一毫秒会重复生成 ID。四种处理方式与各自代价:
| 处理方式 | 做法 | 代价 |
|---|---|---|
| 等待 | 回拨幅度小时自旋等到追上上次时间戳 | 阻塞调用线程,回拨大时不可接受 |
| 抛异常 | 超过阈值(如 5 ms)直接失败 | 发号服务短暂不可用,由上游重试 |
| 备用位 | 预留位记录"回拨次数",用额外位区分同一毫秒 | 消耗位宽,且回拨多次仍会耗尽 |
Leaf-snowflake 校验 | 启动时用 ZooKeeper 顺序节点写入 workerId 并校验时间戳,回拨时报警 + 拒绝发号(并等 ZK 时间对齐) | 引入 ZK 依赖;只解决启动期冲突,运行期回拨仍要策略 |
按四个维度给结论:要严格有序(如数据库主键、分页游标)→ 数据库自增或号段;要高性能且趋势有序(如订单号)→ Snowflake 系;不想有任何中心依赖 → Snowflake;已有 Redis 且量不大 → INCR。工程上最常见的是 Leaf-segment 打底 + Snowflake 兜高并发:号段双 buffer(当前号段用到 10% 就异步预取下一段)避免取号时的请求尖峰,并支持动态调整步长(按最近一段的消耗速度线性外推,流量涨就调大步长)。 |
提示
面试官会追问"Leaf-segment 的双 buffer 到底解决什么"。答:它把"取号"这个 DB 往返从关键路径上摘掉——单 buffer 时每个号段耗尽的瞬间都要同步查一次 DB,那一瞬间的 QPS 尖峰与 DB 抖动会直接传导成接口毛刺;双 buffer 让预取发生在号段还有余量时,DB 慢一点也有缓冲。这正是"用一点内存换掉尾延迟"的典型例子。
34.7 分布式锁:四种实现与各自不安全的时刻
本节只做理论对比与缺陷判定;
Redis锁的命令写法、看门狗与主从切换细节见第 27 章 27.11 节。
实现 互斥来源 自动释放 性能量级 什么时候不安全 数据库唯一索引 唯一约束 INSERT冲突无(必须显式 DELETE,进程崩溃即死锁)一次写入约 1~10 ms 量级 进程崩溃后锁永久残留; DELETE语句本身可能超时;无超时机制时依赖人工清理RedisSET NX PX的原子写入有( PX过期)一次往返约 0.1~0.5 ms(同机房) 主从异步复制:主库写入成功未同步即宕机,从库升主后另一客户端也能加锁成功,互斥破裂 ZooKeeper临时顺序节点 + Watch有(会话超时即删临时节点) 一次操作约 1~5 ms 量级 会话超时抖动导致误判持锁方已死;羊群效应(若都用 Watch同一节点,释放时惊群)——用顺序节点各自Watch前一个解决etcd租约 Lease+ 事务Txn有(租约到期) 与 ZK同量级本质同 ZK:租约到期与"旧持有者仍在写"之间的窗口依然存在(GC长暂停会放大它)Redis与ZK/etcd的根本差别是一致性模型的差别:Redis主从异步复制走的是 AP 路线,“加锁成功"这个事实在故障切换时可能丢失;ZK/etcd基于共识协议走 CP,“加锁成功"必然是多数派已持久化的事实,所以它在切换后不会丢。这就是同一把锁在两种底座上"安全性不同"的根源,而不是命令写得对不对。Redlock想用"向 N 个(通常 5 个)相互独立的Redis实例依次加锁,多数派成功且在总耗时小于锁有效期内才算成功"来绕开单主从的复制丢失问题。Kleppmann 提出两条质疑:其一,它依赖时钟假设——锁的有效性靠各实例的过期时间推算,而这些进程的时钟可能发生跳变(NTP校时、虚拟机迁移、GC暂停都可能导致本地时间"快进”),续期与过期判断因此在理论上不可靠;其二,它没有 fencing token——一个进程在GC长暂停后恢复,它不知道自己已经不再持锁,仍会向共享资源发起写操作,而资源侧无法识别这是过期持有者。Antirez 的回应是:不依赖时钟的严格方案需要额外机制,而Redlock的时钟误差假设(各实例时钟漂移远小于锁有效期)在其设计边界内是可以接受的工程近似。结论(本节的加分点): Redlock争议的真正价值不在于判谁赢,而在于它揭示了一条工程准则——锁的正确性不应被用来保护数据一致性,除非配合fencing token。若锁只是"减少重复计算”(如避免缓存击穿时的重复回填),出错的代价只是多做一次;若锁保护的是数据一致性(如"同一订单只能被处理一次"),必须让下游资源能拒绝过期锁的写入:持锁方从锁服务拿到单调递增的版本号,写请求带上它;资源侧(数据库、存储)记录已见到的最大版本号,拒绝任何版本号更小的写入。
| |
重要
这就是"分布式锁"一节的最终答案:锁本身不可能保证绝对互斥(时钟、暂停、网络都会破坏它),能保证的只有"资源侧的单调版本号校验"。面试里主动说出这一层,比多背十种锁实现更有区分度。
34.8 限流算法与一致性哈希
34.8.1 四种限流算法
| 算法 | 通过速率 | 突发容忍 | 实现成本 | 主要缺陷 |
|---|---|---|---|---|
| 固定窗口计数 | 窗口内均值 | 无(窗口边界处可 2×) | 极低(一个计数器) | 临界点双倍:窗口 1 s、阈值 100,则 0.99 s 来 100 个 + 1.01 s 来 100 个,在 0.02 s 内通过 200 个 |
| 滑动窗口 | 窗口内均值 | 无 | 中(Redis ZSet 或环形数组) | ZSet 方案内存随请求数增长(每请求一个成员),需按时间清理;环形数组方案精度取决于格子数 |
| 漏桶(leaky bucket) | 恒定流出 | 不容忍(缓冲后匀速放行) | 低 | 突发被缓存在桶里,桶满即丢;恒定速率无法利用空闲容量 |
| 令牌桶(token bucket) | 平均速率 | 允许,上限为桶容量 | 低 | 需要维护令牌生成速率与桶深两个参数,桶深即"允许多大的瞬时突发" |
固定窗口的临界点双倍必须给数字:阈值 100 QPS,窗口按自然秒对齐。客户端在 t=0.95 s 打 100 个(第 1 个窗口),在 t=1.05 s 再打 100 个(第 2 个窗口),两个窗口各自都没超 100,但在 0.1 s 内实际通过了 200 个请求,瞬时速率达到 2000 QPS——这是固定窗口唯一但致命的漏洞。滑动窗口把它按时间切成 N 个格子(如 1 s 切 10 格、每格 100 ms),统计"当前时间往前一整窗"的计数总和,把边界效应压到 1/N 的粒度。 | ||||
令牌桶的容量公式值得记牢:桶深 B、令牌生成速率 r,则在无流量期间最多攒 B 个令牌,因此允许的最大瞬时突发是 B 个;长期平均速率被钳在 r。Guava RateLimiter 的单机实现用的是"惰性计算"——不真的起定时器加令牌,而是记录 nextFreeTicketMicros,每次请求时按经过时间算应生成多少令牌,把开销降到 O(1) 且无后台线程。分布式实现则必须把"令牌数与上次填充时间"存到 Redis 并用 Lua 脚本原子地读-算-写,否则并发下会把同一批令牌发两次。 | ||||
分布式限流的两种思路:集中式(所有实例共用 Redis 计数器,一致性好,但每次限流判定多一次 Redis 往返,且 Redis 成为限流热 key 与单点——单 key 每秒几万次 INCR 就要考虑分片或本地预取);配额分配(中心按实例数把配额切开,每实例本地限流,如 1000 QPS ÷ 10 实例 = 各 100,代价是流量不均时部分实例饿死、部分实例超发,靠定期上报与动态调整缓解;也可以让实例按需向中心"借"配额)。 | ||||
限流的维度:接口(/api/order/create)、用户(UID/租户)、IP、以及它们的组合(“每用户每接口”)。维度选择决定键的设计,键太细会让计数爆炸、太粗会让单用户拖垮全局。限流后的行为不止"拒绝"一种:排队(把请求放进有界队列稍后处理,适合同步调用转异步)、降级(返回缓存或默认值)、以及返回 429 并带 Retry-After 让客户端退避。 |
注意
面试官会追问"网关限流和业务限流为什么要两层"。答:网关限流是粗粒度的保护(挡住明显异常流量、保护整个集群),业务限流是细粒度的公平性(保护下游依赖、按用户/租户分配配额)。只做网关限流会让单个大客户占满全网配额,只做业务限流会让异常流量打到业务层才被拒。
34.8.2 一致性哈希、虚拟节点与 Redis Cluster 的分片
普通取模扩容的代价可以精确量化。 用 hash(key) mod N 分片,从 N 个节点扩到 N+1 个节点时,某个键仍落在同一节点,当且仅当 hash(key) mod N == hash(key) mod (N+1)。在哈希均匀的近似下,这个比例约为 1/(N+1),因此需要迁移的键的比例约为 N/(N+1):
N=3 → 4:迁移3/4 = 75%;N=9 → 10:迁移9/10 = 90%;N=99 → 100:迁移99/100 = 99%。 也就是说,节点越多,扩一个节点几乎要搬全量数据(N取 100 时是 99%),这是"取模分片不能平滑扩容"的量化结论。 一致性哈希(consistent hashing) 把哈希空间组织成一个0 ~ 2³² − 1的环,节点与键都哈希到环上,键归属顺时针方向第一个节点。增删一个节点时,只有该节点逆时针到前一个节点之间的那一段键需要迁移,期望迁移比例是1/(N+1)(加一个节点时约分走1/(N+1)的键)。
| |
一致性哈希的问题是数据倾斜:节点数少时,节点在环上分布不均,可能出现一个节点承担 50% 以上的键(“一个节点管半个环”)。极端情况下两个节点哈希得很近,其中一个几乎分不到数据。
虚拟节点(virtual node) 解决它:每个物理节点映射为 V 个虚拟节点(V 通常取 100~1000,工程上常用 150 或 160),虚拟节点散布在环上,键先落到虚拟节点再映射回物理节点。V 越大,各物理节点承载的键比例越接近 1/N——这是"用更多哈希点逼近均匀分布"的做法。代价是每个物理节点的信息要存 V 份(N 个节点 V=160 时是 160N 个环上条目),路由表内存与查找成本随之上升;Ketama 算法就是在这一层做优化的经典实现(它把每个物理节点的虚拟节点数固定为 160,并用一个有序结构做二分查找来定位顺时针第一个节点)。
警告
Redis Cluster 的 16384 个槽(slot)不是一致性哈希环,别把两者混为一谈。它是固定分片(fixed sharding):槽号由 CRC16(key) mod 16384 得到,槽再静态分配给主节点,扩容时是按槽迁移(MIGRATE)而不是按哈希环区间迁移。为什么是 16384 而不是 65536:集群用位图(bitmap)表示"每个节点负责哪些槽",16384 个槽只需 16384/8 = 2 KB,65536 则需要 8 KB,心跳包每次都要带这份位图,节点数多时开销明显;同时 Redis Cluster 设计上支持的节点数在千级量级,16384 个槽在"每节点分到足够多的槽"与"位图开销"之间是折中。答题时把"一致性哈希环"和"槽分片"分成两套机制来答,是这一节的分界线。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| CAP 的准确含义 | C 是线性一致,A 是有界非错误响应,P 是网络分区容忍;分区时在 C 与 A 间二选一 | 答"三选二"、把 A 答成"高可用" |
| PACELC | 分区时选 A 或 C;无分区时选延迟 L 或一致性 C | 只谈 CAP,忽略日常态的 L/C 权衡 |
| 一致性谱系 | 线性一致 = 顺序一致 + 真实时间约束 | 说不清线性一致与顺序一致的区别 |
| Raft 提交规则 | 只有当前任期的日志可凭多数派直接提交,旧任期只能被连带提交 | 答"复制到多数派即可提交" |
| Raft 选举 | 随机化超时打散同时超时,避免选票瓜分;投票要求日志不落后 | 答"超时越短越快",说不出为什么要随机 |
| 分区下的双主 | 少数派侧无法提交、恢复后回滚未提交日志 | 认为两个 Leader 都会推进状态机 |
Paxos 与 Raft | 同一约束(多数派 + 单调编号)的不同表达;ZAB 是主备复制协议 | 把 ZAB 与 Raft 说成一回事 |
| 分布式事务选择 | 短事务要强一致用 2PC/TCC,长流程用 Saga,最简是本地消息表 | 认为 Saga 提供隔离性;忘记 TCC 的空回滚与悬挂 |
| 分布式 ID | 严格有序选自增/号段,高性能无序依赖选 Snowflake | 忘记 Snowflake 的时钟回拨,答不出 4096/ms 的来源 |
| 分布式锁 | 锁不可能绝对互斥,要一致性必须配 fencing token | 只答"Redis 快用 Redis",不知主从切换会双持有 |
| 限流 | 固定窗口有临界点双倍;令牌桶用桶深控制突发 | 认为固定窗口就够用,或说不清桶深与速率的关系 |
| 分片 | 取模扩容迁移 N/(N+1);一致性哈希迁移 1/(N+1),靠虚拟节点治倾斜 | 把 Redis Cluster 的 16384 槽说成一致性哈希环 |
自测题
Q1(辨析)有人说"我们的系统选择了 AP,所以不需要线性一致,只要最终一致"。这句话在 CAP 的语境下错在哪?
答案与解析
错在两处。第一,CAP 的选择只在分区期间生效,无分区时说的是 PACELC 的第二段(L 与 C 的权衡),不能拿"我们选 AP"来为日常态的数据陈旧开脱——日常态的陈旧是 L/C 权衡的结果,需要单独解释(例如异步复制换低延迟)。第二,“选 AP"只承诺了分区时的可用性偏好,并不蕴含"只能做到最终一致”。因果一致通常可以在 AP 系统里实现(例如用版本向量或依赖跟踪),而且业务上"最终一致"必须给出收敛时间上界与冲突解决策略(LWW、版本向量、CRDT),否则不是模型而是一句口号。面试官会紧接着追问"你们的最终一致窗口是多大、冲突怎么解",答不出上界就是背概念。
Q2(推导)5 节点 Raft 集群,Leader S1 在任期 2 追加了 idx2=term2,只复制到 S2。随后 S1 宕机,S5 在任期 3 当选并追加 idx2=term3,只复制到 S3、S4,然后 S5、S3、S4 全部宕机。S1 重启后在任期 4 再次当选并把 idx2=term2 复制到全部 5 个节点。此时能否说"idx2=term2 已在多数派上,可以提交"?为什么?
答案与解析
不能。idx2=term2 虽然物理上位于多数派上,但它所在位置的多数派可能已被后续任期替换:S5 那条 idx2=term3 曾经在 {S3,S4,S5} 这个多数派上存在过,若允许"旧任期条目凭计数提交",则 idx2=term2 被提交后会连带提交 idx2=term3;而 S5 一旦再次当选并复制日志,会用自己的版本覆盖 idx2=term2,于是"已提交的条目被覆盖",状态机安全性破裂。Raft 的规则是只允许提交当前任期的条目:当前任期条目能过多数派,就意味着多数派已包含它之前的全部条目(prevLogIndex/prevLogTerm 检查保证),而未来任何 Leader 都必须从这个多数派中产生,必然带有这些条目,安全性由此得到保证。追问层次:面试官下一步会问"那旧任期日志怎么才算提交"(答:当前任期条目提交后被连带提交),再问"如果一直没有新写入,commitIndex 停住怎么办"(答:新 Leader 提交一条 no-op 条目推进)。
Q3(计算)某服务用 hash(key) mod N 分 8 个库。现在要扩到 9 个库,问:(1) 约多少比例的键需要迁移?(2) 若改用一致性哈希(无虚拟节点),加第 9 个节点时期望迁移比例是多少?(3) 如果一致性哈希出现严重倾斜,加虚拟节点后迁移比例会怎么变?
答案与解析
(1) N=8 → 9,迁移比例约 N/(N+1) = 8/9 ≈ 88.9%——几乎全量搬迁。推导:某键不迁移要求 hash(k) mod 8 == hash(k) mod 9,在均匀哈希下这一比例约为 1/9,故迁移 8/9。这是"取模分片不能平滑扩容"的量化依据,也是面试官要的"迁移比例"数字。(2) 一致性哈希下新增节点只接管它逆时针到前驱之间的区间,期望迁移比例约 1/9 ≈ 11.1%,比取模低约 8 倍。(3) 虚拟节点不改变期望迁移比例(仍约 1/(N+1)),它改变的是分布的均匀性:不加虚拟节点时单个物理节点可能承担 40% 以上的键,这会让新节点接管区间的方差极大(有的节点几乎不动、有的搬走大半);加上 V=150 量级的虚拟节点后,各节点承载比例的标准差约下降 √V 倍量级,迁移量从"取决于运气"变成"接近期望值"。追问点:Redis Cluster 的 16384 槽是按槽迁移的固定分片,不是哈希环,扩容的迁移量取决于你迁移了多少个槽。
Q4(设计)一个跨服务的订单扣库存场景,要求"同一订单绝不重复扣减"。团队打算用 Redis 分布式锁(SET NX PX + Lua 释放)保护扣减逻辑,你如何评价这个方案?如果一定要用锁,怎么补齐它的短板?
答案与解析
方案不充分,因为它把数据正确性押在锁的可靠性上,而 Redis 锁在至少两种场景下会失效:主从异步复制丢失锁(主库加锁成功后未同步即宕机,从库升主后另一客户端也能加锁成功,两方同时扣减),以及**GC 长暂停或网络分区导致的锁过期**(旧持有者恢复后仍以为持锁并继续写)。补齐方式有四层:其一,把真正的幂等保证下沉到存储层——用唯一索引或 UPDATE ... WHERE status = 'INIT' 这类条件更新,让"重复扣减"在数据库层面直接失败(接口幂等的完整落地见第 35 章);其二,若锁只用于减少竞争,就承认它是优化而非正确性保证;其三,若锁确实要保正确性,必须引入 fencing token:每次加锁返回单调递增的版本号,写请求带上它,存储侧记录已见的最大版本号并拒绝更小的版本号;其四,锁的失效要可观测(续期失败、租约到期要告警),并配业务侧的对账补偿兜底。追问链条:Redlock 能不能解决?答:它缓解了单主从切换的丢锁问题,但依赖各实例时钟漂移远小于锁有效期的假设,且依然没有解决"旧持有者不知情仍在写",所以不能替代 fencing token。
Q5(辨析)Paxos 的 Basic 版本既然能保证安全性,为什么工程上几乎所有系统都用 Raft 或 Multi-Paxos 而不是 Basic Paxos?请给出两个以上的具体原因。
答案与解析
三点。其一,Basic Paxos 只对"一个值"达成共识,复制日志需要对每个日志槽位反复跑两阶段,而多个提议人并发提交时会出现活锁——P1 与 P2 互相用更大编号抢占,谁都无法完成 Phase 2,虽然安全性不破但永远达不成决议。其二,Basic Paxos 没规定 Leader,任何节点都能提案,因此没有"日志只能从主单向流向从"的性质,实现者必须自己补上日志空洞的处理、提案顺序、成员变更等大量细节;Multi-Paxos 通过选出 distinguished proposer 并把 Phase 1 一次做完来降为稳态 1 RTT,而 Raft 直接把这个优化写进了协议(强 Leader + 连续日志 + prevLogIndex 检查)。其三,可理解性与可实现性是设计目标:Raft 把 Leader 选举、日志复制、成员变更、快照拆成相互独立、可分别实现与测试的模块,并提供明确的正确性论证路径,这显著降低了实现出 bug 的概率——分布式共识的实现细节极多,一个不留神的状态判断就会破坏安全性。面试官可能追问"ZAB 与 Raft 的差别",答:ZAB 是主备模型的状态复制协议(zxid 连续、差异同步),Raft 是领导者选举模型,二者共享多数派与单调编号的核心约束。
面试高频问题与陷阱
| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应本章小节 |
|---|---|---|---|
| 请说说 CAP 定理,为什么说"三选二"? | 是否知道流行表述本身就是误读 | 错答"三个只能选两个,我们选了 AP"——把 P 当成可选项,也说不清 A 的准确定义;加分点:指出 P 在分布式系统中是前提,真实选择只在分区期间于 C 与 A 之间,并主动引出 PACELC | 34.1 |
| CAP 里的 A 到底指什么?它和系统可用率 99.99% 是一回事吗? | 定义精确性 | 错答"就是高可用/不宕机";正确答法是"每个非故障节点在有界时间内返回非错误响应",并指出超时与 503 都不算 A,而返回陈旧数据算 A | 34.1.1 |
| 线性一致和顺序一致有什么区别?能举个具体例子吗? | 谱系中最易混淆的两档 | 错答"线性一致更强,两个都要保证顺序"却举不出例子;加分点:给出"写在 t=10 返回、读在 t=20 开始却读到旧值"这个历史,指出它在顺序一致下合法、线性一致下非法,并总结"线性一致 = 顺序一致 + 真实时间约束" | 34.2 |
| Raft 的 Leader 什么时候可以提交一条日志?旧任期的日志能不能直接提交? | Raft 最精妙的点 | 错答"复制到多数派就可以提交";加分点:说出"只有当前任期的日志能凭多数派提交,旧任期只能被连带提交",并用 Figure 8 式反例推出"否则已提交条目会被覆盖" | 34.3.4 |
| 为什么 Raft 的选举超时要在 150~300 ms 之间随机?固定超时会怎样? | 是否理解机制动机 | 错答"随机是为了更快选出 Leader";正确答法是打散同时超时、避免选票瓜分导致无限重试,并能说出"每轮重试独立、冲突概率随之下降" | 34.3.2 |
| Paxos 和 Raft 是什么关系?ZAB 和 Raft 有什么区别? | 协议谱系与辨析 | 错答"Raft 就是 Paxos 的实现"或"ZAB 和 Raft 一样";加分点:Paxos 解决单值共识且 Basic 有活锁,Raft 是可理解性导向的 Multi-Paxos 变体,ZAB 是主备复制协议 | 34.4 |
你们线上用的是 2PC、TCC 还是 Saga?为什么? | 方案选型与不一致窗口 | 错答"我们用 TCC,它保证强一致"(TCC 是最终一致);或答 Saga 时不说它没有隔离性。加分点:给出各方案的不一致窗口具体形态(2PC 的不确定态、TCC 的业务中间态、Saga 的中间态暴露) | 34.5 |
TCC 的空回滚、幂等、悬挂分别是什么?怎么处理? | 是否落地过 | 错答只知道"三段提交",说不出空回滚是 Cancel 先于 Try 到达;加分点:用"防悬挂标记"同时解决空回滚与悬挂,并说明三个操作都必须幂等 | 34.5 |
| 雪花算法的 ID 结构是什么?时钟回拨了怎么办? | Snowflake 细节 | 错答"41 位时间戳 + 10 位机器 + 12 位序列"但说不出 4096/ms 与约 69 年的来源;对回拨只答"抛异常";加分点:给出等待/抛异常/备用位/Leaf-snowflake 的 ZK 校验四种策略及各自代价 | 34.6 |
分布式锁用 Redis 还是 ZooKeeper?Redis 锁会出什么问题? | 锁的可靠性边界 | 错答"Redis 快所以用 Redis";加分点:说出主从异步复制导致的双持有、GC 暂停导致的过期锁,以及 Redlock 的两条争议(时钟假设、无 fencing token),最后给出"保一致性必须配 fencing token"的结论 | 34.7 |
| 固定窗口限流有什么问题?令牌桶和漏桶的区别是什么? | 算法缺陷与参数语义 | 错答"固定窗口够用了";说不出临界点双倍要给出"0.99 s 打 100 个 + 1.01 s 打 100 个"的数字;漏桶答成"允许突发"(那是令牌桶),说不出桶深 B 就是允许的最大瞬时突发 | 34.8.1 |
一致性哈希解决什么问题?虚拟节点为什么必要?Redis Cluster 用的是什么? | 分片机制 | 错答"一致性哈希就是为了均匀分布"(均匀分布要靠虚拟节点);算不出取模扩容迁移 N/(N+1)、一致性哈希约 1/(N+1);把 Redis Cluster 的 16384 槽说成一致哈希环 | 34.8.2 |
延伸阅读
In Search of an Understandable Consensus Algorithm(Raft 论文,Ongaro & Ousterhout):34.3 全节的原始依据,Figure 8 是 34.3.4 反例的来源。Paxos Made Simple(Lamport):34.4 节两阶段的原始表述。Zab: High-performance broadcast for primary-backup systems:34.4 节ZAB与 Raft 差异的依据。Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services(Gilbert & Lynch):34.1 节 CAP 的形式化证明。Consistency Tradeoffs in Modern Distributed Database System Design(Abadi,PACELC 的提出):34.1.3 节。How to do distributed locking(Kleppmann)与Is Redlock safe?(Antirez):34.7 节Redlock争议的正反两方原文。Redis Cluster Specification:34.8.2 节16384槽与位图开销、按槽迁移的官方依据。Apache Seata官方文档(AT/TCC/Saga模式与全局锁、undo_log):34.5 节。- 见第 21 章 21.5 节(两阶段提交的崩溃点恢复)、第 22 章 22.1 节(ACID 的实现依赖)、第 23 章 23.2 节(随机主键导致页分裂)、第 26 章 26.1 节(复制模式与丢数据窗口)、第 27 章 27.11 节(
Redis锁的命令实现与看门狗)、第 33 章 33.4 节(重复消费与幂等原语)、第 35 章(接口幂等落地与熔断三态)。