分布式锁与共识算法:从一把锁到一群节点的一致
分布式锁与共识算法:从一把锁到一群节点的一致
单机防并发,
synchronized一把锁就够;可应用扩到十台机器,这把锁就锁不住了——它只管得了自己进程里的线程,管不了另外九台机器同时冲进来的请求,超卖就这么来的。这篇分两层:上半场讲分布式锁(跨机器都管用的锁怎么造、Redis 和 etcd 各怎么做、容易踩哪些坑),下半场挖底层共识算法(多个节点怎么对"某个值"达成一致)。
一、分布式锁:跨机器的那把锁
是什么。 分布式锁,就是一把所有机器都认、都看得见的锁。单机锁的"锁状态"存在进程自己的内存里,出了这个进程谁也看不见;分布式锁则把锁状态放到一个所有节点都能访问的外部存储里(Redis、ZooKeeper、etcd、数据库都行),谁先在那里"抢到标记",谁就算持有锁,其余的人要么等、要么直接失败。本质上,它是把"互斥"这件事,从单机内存挪到了一个公共的、所有人共享的地方。
为什么需要它——绕不开的就是"防重复执行"。 多机部署下,这几类场景没有分布式锁就会出事:
- 库存扣减、防超卖。 最后一件商品,两台机器同时判断"还有货"、同时扣减,结果卖出去两件——这正是《缓存》篇里反复强调的"库存这种强一致数据别简单缓存"的延伸:真到要扣减那一下,得有人保证同一时刻只有一个人在动这件库存。
- 防重复下单 / 重复支付。 用户狂点提交、或网络重试,同一笔订单的请求落到不同机器上,稍不留神就建出两单、扣两次款。
- 定时任务防并发。 一个跑批任务部署在三台机器上,到点了三台都触发——同一份对账跑了三遍。分布式锁让"同一时刻只有一台机器在跑这个任务"。
一句话:单机锁锁的是"线程",分布式锁锁的是"进程/机器"。 业务从一台扩到多台的那一刻,前者就自动失效了。
注意,锁不是唯一解,甚至常常不是最优解。 这点很重要,放在最前面说:分布式锁是有成本、有坑的(下一节全是坑),很多看似"要加锁"的场景,其实用唯一索引、乐观锁、或者把请求串成队列就能更便宜地解决——这个我们在第三节专门讲"能不用锁就别用"。先把锁讲清楚,再讲怎么尽量躲开它。
二、业界怎么做:Redis 实现 与 ZooKeeper/etcd 实现
分布式锁的实现,业界主流就两大流派:一类是基于 Redis(图快、最常用),一类是基于 ZooKeeper / etcd(图稳、一致性更强)。先看原理,再在第六节用一张表对比。
流派一:Redis 实现——SET key value NX EX。 这是最常见的一把分布式锁,核心就一条命令:
SET lock_key 唯一标识 NX EX 30
拆开看:NX 表示"key 不存在时才设置成功"(这就天然实现了互斥——谁先 SET 成功谁拿锁);EX 30 给这个 key 设 30 秒过期时间(防止持锁的机器突然宕机、锁永远不释放);唯一标识(比如一个随机 UUID)用来标明"这把锁是我加的"。释放锁就是把这个 key 删掉。关键点是这三件事要在一条命令里原子完成——早年有人先 SETNX 再单独 EXPIRE,结果两条命令中间机器挂了,锁就成了永不过期的死锁,所以现在统一用 SET ... NX EX 一步到位。
Redis 锁胜在快、简单、几乎人人都有 Redis,是绝大多数业务的默认选择。但它有个绕不开的软肋:Redis 主从架构下,主节点拿到锁还没来得及同步给从节点就宕机了,从节点被提升为主,新主上根本没有这把锁的记录,于是另一个人又能拿到同一把锁——锁就破了。为了解决这个,Redis 作者提出了 Redlock(红锁)算法:不依赖单个 Redis,而是向 N 个相互独立的 Redis 节点(比如 5 个)同时申请锁,拿到多数(≥3 个)才算加锁成功。它的争议很大,我们留到第三节细说。
流派二:ZooKeeper / etcd 实现——临时顺序节点。 这一流派的一致性更强,代价是更重一些。以 ZooKeeper 为例,它的招牌做法是临时顺序节点(ephemeral sequential node):
- 每个想加锁的客户端,都在同一个目录下创建一个临时顺序节点,ZooKeeper 会自动给它们编号(node-0001、node-0002、node-0003……);
- 规定编号最小的那个节点持有锁,其余的都排队等;
- 排在后面的节点,只盯着它前一个节点(比如 0003 盯着 0002),前一个释放(节点消失)了,就轮到自己。
这套设计有两个很妙的地方。其一,"临时"节点意味着:持锁客户端一旦断线(会话超时),它创建的节点会被 ZooKeeper 自动删除,锁自动释放——根本不需要给锁设过期时间,天然解决了"宕机了锁不释放"的问题。其二,"只盯前一个节点"避免了所有等待者同时被唤醒去争抢(业界叫惊群效应),效率更高。etcd 思路类似,基于它的 lease(租约)+ key 的版本号来实现,云原生体系里用得很多。
怎么直觉地选? 图方便、能容忍极端情况下偶尔锁失效 → Redis 锁;对一致性要求高、不能容忍"两个人同时拿到锁"(比如选主、关键资源的独占)→ ZooKeeper/etcd 锁。 前者是 AP 倾向(优先可用),后者是 CP 倾向(优先一致)——AP/CP 这组概念在《CAP 与一致性》篇会专门讲,这里先记住:Redis 锁快但理论上可能破,ZK/etcd 锁稳但更重。
三、分布式锁的坑(重点):锁过期、误删、Redlock 争议、能不用就别用
这一节是全篇最想讲透的——分布式锁真正难的不是"加锁",而是这一堆坑。下面这三个,几乎是写 Redis 锁绕不开的。
坑一:锁过期了,业务还没干完。 为防死锁,我们给锁设了 30 秒过期。可万一这次业务特别慢、跑了 35 秒呢?第 30 秒锁自动过期、被释放,第二个人趁机拿到了锁——于是两个人同时在临界区里跑,互斥直接失效。业界的标准解法叫看门狗(watchdog):加锁后起一个后台线程定时去续期(比如每 10 秒把过期时间重置回 30 秒),只要业务还在跑,锁就一直不过期;业务跑完了,看门狗也停。Redisson(Java 里最常用的 Redis 客户端封装)就内置了看门狗机制,默认锁 30 秒、每 1/3 时间续一次,这也是大家爱用它而不愿手写 Redis 锁的原因。
坑二:误删了别人的锁。 接着上面的场景:你的锁过期了、被第二个人拿走了,你的业务这才跑完,手一抖执行了"删 key"释放锁——把第二个人的锁给删了。于是第三个人又能进来……连环错位。解法就是前面埋的伏笔——value 里放唯一标识:释放锁前先比对"这个 key 的 value 是不是我当初写的那个 UUID",是我的才删,不是我的不许动。而且"比对 + 删除"这两步也必须原子执行(通常用一段 Lua 脚本一次跑完),否则比对完、正要删的瞬间锁又过期被别人拿走,照样误删。记住一句:谁加的锁,只能谁来解。
坑三:Redlock 到底靠不靠谱? 前面提到 Redlock 用"多数派 Redis 节点"来增强可靠性。但它引发过一场著名的学术争论:分布式系统专家 Martin Kleppmann(《Designing Data-Intensive Applications》/《数据密集型应用系统设计》的作者)写文章质疑 Redlock 在进程暂停(比如 GC 停顿)和时钟漂移下并不安全——比如你以为还持有锁,其实因为一次长 GC,锁早过期被别人拿走了,而你毫不知情。Redis 作者 antirez 则撰文回应。这场辩论没有谁完全说服谁,但它给所有人提了个醒:没有任何分布式锁能 100% 安全。所以对"绝对不能出错"的操作(扣钱、扣库存),业界的做法是给锁再加一道防线——比如用一个单调递增的 fencing token(防护令牌),让真正执行写入的存储层校验"令牌是不是最新的",过期的旧令牌即使绕过了锁也会被存储层拒绝。锁负责"大部分情况下别冲突",存储层的唯一性兜底"万一冲突了也不出错"。
坑四(也是最该先问自己的):能不用锁,就别用锁。 分布式锁是把"并行"强行掰成"串行",天然牺牲性能,还自带上面这些坑。很多场景根本不需要它:
- 唯一索引兜底。 防重复下单,与其加锁,不如给订单表的"业务唯一键"建一个数据库唯一索引——重复插入直接被数据库的唯一约束挡下,天然幂等。这正是《消息队列》篇和《API 设计》篇里反复讲的那套幂等做法,唯一索引是最稳的兜底。
- 乐观锁(版本号 / CAS)。 更新库存时不加锁,而是
UPDATE ... SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = 旧版本,靠数据库行锁 + 版本号判断"有没有人在我之前改过";改过了就重试。读多写少、冲突不频繁时,乐观锁比分布式锁轻得多。 - 把请求排成队列。 秒杀这类极端并发,常见做法是把请求塞进消息队列削峰、由单一消费者串行处理(《消息队列》篇讲过削峰填谷),从源头上就不存在并发,自然不用锁。
一句话点破:分布式锁是"最后才用"的重武器。先看唯一索引、乐观锁、队列能不能解决——它们更便宜、更不容易出错。
四、共识算法:一群节点怎么对一件事达成一致
讲完锁,往下挖一层。你可能已经注意到:ZooKeeper、etcd 这些"靠谱"的协调组件,它们自己也是多台机器组成的集群——那么问题来了,它们内部那几台机器,是怎么保证"对同一件事看法一致"的? 比如三台 etcd,客户端写了个值,怎么保证三台都认这个值、而不是各说各话?这就要靠共识算法(Consensus Algorithm)。
是什么。 共识算法解决的是分布式系统里最根本的一个问题:让一组节点,对"某个值是什么"或"谁是主节点"达成一致,并且即使部分节点宕机、网络出问题,这个一致也不会被破坏。 注意它和分布式锁的层次不同:**分布式锁是"应用层"借助外部存储实现的互斥,共识算法是那个"外部存储"内部用来保证自己可靠的底层机制。**锁是建在共识这块地基上的。
为什么需要它。 几个绕不开它的场景:
- 选主(Leader Election)。 一个集群得选出一个"主"来统一决策(谁来处理写、谁来协调)。选主本身就是一次共识:所有节点必须就"现在谁是主"达成一致,绝不能出现两个节点都认为自己是主(这就是后面要讲的"脑裂")。
- 配置 / 元数据同步。 集群的配置、服务注册信息这类关键数据,必须在所有节点上完全一致,不能 A 节点说服务在这、B 节点说在那。
- CP 系统的基石。 ZooKeeper、etcd 之所以能当"可信的协调者",正是因为它们内部用共识算法保证了强一致——这也是为什么 etcd 被选作 Kubernetes 存放整个集群状态的核心存储:K8s 把"集群该是什么样"全交给 etcd,而 etcd 用共识算法保证这份状态永远只有一个版本、不会分裂。
场景串起来看。 etcd 选主、ZooKeeper 选主、数据库的主从切换里"谁当新主"的裁决……底下都是同一套共识在工作。理解了共识,你才真正理解前面那些"一致性更强的锁"强在哪——强就强在它们脚下这块地基。
五、业界怎么做:Raft、Paxos、ZAB
共识算法听着玄,主流就那么几个。这里客观陈述、不做推荐,重点把 Raft 的直觉讲明白,因为它是现在最主流、也最好懂的。
Raft——为"可理解"而生的主流方案。 Raft 的设计目标里就明明白白写着一条:让共识算法变得容易理解(它的论文标题直译就是"寻找一种可理解的共识算法")。它把整个问题拆成三块,直觉如下:
- 选举(Leader Election)。 集群里只有一个 Leader(主),其余是 Follower(从)。每个 Follower 都有个倒计时,Leader 会定时发"心跳"重置它们;一旦某个 Follower 超时没收到心跳(可能 Leader 挂了),它就变成 Candidate(候选人)发起选举、拉票。谁先拿到多数票(超过半数),谁就当选新 Leader。 这就是为什么"多数派"如此关键——下一节专门讲。
- 日志复制(Log Replication)。 所有写请求都先到 Leader,Leader 把这条操作当成一条日志,复制给所有 Follower。等多数 Follower 都确认写下了,这条日志才算"提交"(commit)、对外生效。 因为每个节点都按同样的顺序回放同一份日志,所以最终所有节点的状态完全一致。
- 安全性(多数派保证)。 选举和提交都卡"多数派"这条线,保证了任意时刻不会有两个 Leader、也不会丢已提交的数据。
Paxos——更早、更经典、也更难懂。 Paxos 是共识领域的"祖师爷",由 Leslie Lamport 提出,理论地位极高,很多系统的底层思想都源自它。但它出了名地难理解和难正确实现——Raft 的诞生很大程度上就是因为"Paxos 太难教、太难落地了"。所以这里只提一句:Paxos 在前,Raft 在后,Raft 是站在 Paxos 肩膀上、为了好懂而重新组织的产物。
ZAB——ZooKeeper 的专用协议。 ZAB(ZooKeeper Atomic Broadcast,ZooKeeper 原子广播)是 ZooKeeper 自己用的共识协议,和 Raft 在"选主 + 日志/广播 + 多数派"这些核心直觉上高度相似,是为 ZooKeeper 的场景量身定做的。记忆锚点:etcd / Consul 用 Raft,ZooKeeper 用 ZAB,两者神似;Paxos 是它们共同的思想源头。
六、注意事项:多数派、奇数节点、共识不是免费的
共识算法有几条"为什么这么设计"的常识,搞懂了能少很多困惑。
为什么必须"多数派(quorum)"? 共识的核心规则是:一个决定要超过半数节点同意才生效。 为什么是多数、不是全体?因为要全体同意,只要挂一个节点整个集群就卡死了,太脆弱;而"多数派"巧妙之处在于——任意两个多数派之间,必然至少有一个公共节点。正是这个重叠节点,保证了不会出现"这边多数派认 A、那边多数派认 B"的分裂局面。多数派,是共识能容错又不分裂的数学根基。
为什么节点数常是奇数(3、5、7)? 因为"多数"是按总数算的:3 个节点容许挂 1 个(剩 2 个仍过半);5 个容许挂 2 个。 关键是——4 个节点也只能容许挂 1 个(剩 3 个才过半,挂 2 个就只剩 2 个、不过半了),容错能力和 3 个一样,却白白多养一台机器、还多一份网络开销。所以奇数节点在"容错能力 / 成本"上最划算,这是 etcd、ZooKeeper 集群几乎都用奇数台的原因。
为什么共识不是免费的? 这是最该记住的一条:共识用一致性换走了性能。 每一次写,都要等"多数节点确认"才能返回——这意味着每个写请求都背着多轮网络往返的延迟,集群越大、节点离得越远,越慢。所以共识系统(etcd/ZooKeeper)适合存"少而关键"的数据(配置、元数据、锁、选主结果),绝不适合扛业务大流量的高频读写——拿它当主数据库存订单,会被写延迟拖垮。
脑裂(Split-Brain),共识要拼命避免的噩梦。 网络故障把集群切成了互不相通的两半,两边各自选出了一个 Leader,各写各的、数据从此分叉——这就是脑裂,分布式系统里最危险的故障之一。多数派机制正是脑裂的克星:网络分区后,只有"占多数那一半"才能凑齐多数票、选出 Leader 并继续工作,占少数的那一半永远凑不齐多数、只能停摆。于是全局始终只有一个 Leader,数据不会分叉。这也再次印证了为什么"多数派"是共识的命门。
一句话收束:共识算法的全部精巧,都在围绕"多数派"做文章——用它容错、用它防脑裂、用它在性能和一致之间踩出一条能落地的线。
七、一张表
把全篇收进两张表。先看分布式锁的两大流派:
| 维度 | Redis 锁(SET NX EX / Redlock) | ZooKeeper / etcd 锁(临时顺序节点) |
|---|---|---|
| 一致性倾向 | AP 倾向,理论上极端情况可能破锁 | CP 倾向,一致性更强 |
| 性能 | 高,一条命令搞定 | 相对低,要走集群共识 |
| 复杂度 / 依赖 | 低,几乎人人有 Redis | 高,要额外维护 ZK/etcd 集群 |
| 锁自动释放 | 靠过期时间 + 看门狗续期 | 靠临时节点,客户端断线自动删 |
| 典型场景 | 普通防重、防超卖(配唯一索引兜底) | 选主、关键资源独占、强一致要求 |
再看三种主流共识算法:
| 算法 | 谁在用 | 一句话特点 |
|---|---|---|
| Raft | etcd、Consul | 为"可理解"而生,选举 + 日志复制 + 多数派,现在最主流 |
| Paxos | 偏理论 / 早期系统 | 共识鼻祖,理论经典但极难懂、难实现 |
| ZAB | ZooKeeper | ZooKeeper 专用,核心直觉与 Raft 神似 |
一句话收尾:分布式锁解决"跨机器互斥",共识算法解决"跨节点一致";前者是后者地基上盖的房子,而那块地基的全部秘密,就藏在"多数派"三个字里。
名词解释
- 分布式锁(Distributed Lock):把锁状态放到所有节点共享的外部存储(Redis/ZK/etcd)里,实现跨进程、跨机器的互斥;单机锁只管本进程,多机部署下会失效。
SET key value NX EX:Redis 实现分布式锁的核心命令——NX保证只有 key 不存在时才设置成功(互斥),EX给锁设过期时间(防死锁),value 放唯一标识(防误删),三者原子完成。- Redlock(红锁):Redis 作者提出的分布式锁算法,向多个相互独立的 Redis 节点申请、拿到多数才算加锁,以增强可靠性;其安全性在学界有争议。
- 看门狗(Watchdog):加锁后起一个后台线程定时给锁续期,只要业务还在跑锁就不过期,解决"锁过期了但业务没干完"的问题(如 Redisson 内置)。
- fencing token(防护令牌):一个单调递增的令牌,让存储层校验"写入者持有的是不是最新令牌",过期的旧令牌即使绕过锁也会被拒,给分布式锁兜底。
- 乐观锁:不加锁,靠版本号 / CAS 在更新时判断"有没有人在我之前改过",冲突就重试;读多写少、冲突少时比分布式锁轻。
- 共识算法(Consensus Algorithm):让一组节点对"某个值"或"谁是主"达成一致,即使部分节点宕机、网络出问题也不破坏;是分布式锁、选主、CP 系统的底层基石。
- 选主(Leader Election):集群选出唯一一个主节点统一决策的过程,本身就是一次共识,必须保证全局只有一个主。
- Raft:目前最主流、最易理解的共识算法,把问题拆成"选举 + 日志复制 + 安全性(多数派)";etcd、Consul 采用。
- Paxos:最早的经典共识算法(Leslie Lamport 提出),理论地位极高但出了名地难懂、难实现;Raft 是为好懂而对它的"重制"。
- ZAB(ZooKeeper Atomic Broadcast):ZooKeeper 专用的共识协议,核心直觉(选主 + 广播 + 多数派)与 Raft 高度相似。
- 多数派 / quorum(法定人数):一个决定要超过半数节点同意才生效;任意两个多数派必有公共节点,这保证了集群既能容错又不会分裂。
- 奇数节点:共识集群常用 3/5/7 台——偶数台不会提升容错能力(4 台和 3 台都只容许挂 1 台),奇数在"容错 / 成本"上最划算。
- 脑裂(Split-Brain):网络分区把集群切成两半、两边各自选出 Leader、数据分叉的严重故障;多数派机制通过"只有占多数的一半能工作"来避免它。
本文属《研发都要懂的事》·服务端架构设计系列——讲分布式协调的两层:上层的分布式锁(跨机器互斥)与底层的共识算法(跨节点一致)。库存扣减、超卖那条强一致主线见《缓存》篇;幂等(唯一索引 / 幂等键)的完整做法见《消息队列》篇与《API 设计》篇。下一阶段继续往下:CAP 与一致性、分布式事务……
评论(0)
登录后参与评论。
还没有评论,来抢沙发吧。

