图 1:本文封面,排队、盯前驱、人走节点消——三个细节就是答案本身。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。这道题的高频之处在于它不止考「会不会用」,而是追问两个设计动机:为什么节点要临时?为什么还要顺序?
想象面试现场:「用 Zookeeper 实现一个分布式锁,说说你的方案。——为什么用临时顺序节点,普通节点行不行?」
核心结论一句话
临时节点解决「持有者死了锁不释放」的死锁问题,顺序节点解决「释放时全员惊醒」的惊群问题。两个修饰词各管一个故障,缺一个,锁就在某个角落埋雷。
先把抢锁流程走一遍。
所有客户端约定一个锁目录(比如 /lock)。第一步,每个想拿锁的客户端都在目录下创建一个临时顺序节点,Zookeeper 会在节点名后追加一个单调递增的序号(十位补零的格式),序号由父节点维护,天然不会撞号。第二步,客户端列出目录下所有子节点,看自己是不是序号最小的那个:是,就拿到了锁,开始干活。第三步,不是最小就挂起等待——但只对自己序号的前一个节点注册监听(watch),听完就睡。第四步,前驱节点消失(正常释放或宕机被清理)时,监听触发,恰好唤醒排在它后面的这一个等待者,醒来再查一次自己是不是最小,是则上位。
动机一:临时节点,防的是死锁。
临时节点的生命线绑在会话上:会话活着节点就在,会话结束节点就被 Zookeeper 自动删除。客户端宕机、网络分区,最终都会走到会话过期,锁节点随之消失,后来者得以继续。这就是「锁不会因人消失而永远占着」。如果换成持久节点,持有者一宕机,节点永远留在那里,除非外部介入,否则锁死到底。官方文档里还有一层细节:会话过期由集群判定,过期时会删掉该会话的全部临时节点,并立即通知所有盯着它们的客户端——释放信号是服务端主动推的,不靠死者自己善后。
动机二:顺序节点,防的是惊群。
不用顺序号时,等待者只能监听锁本身:锁一释放,成百上千个等待者同时被唤醒、同时去抢,只有一个成功,其余全部白醒——这就是惊群效应,锁越热,浪费越大。有了顺序号,每个等待者只监听自己的前驱,释放事件只唤醒一个后继,通知量从「全员广播」降到「点对点」。序号还顺带给出了公平性:先来先得,先到先得,FIFO 语义不用额外实现。
图 2:抢锁四步与临时、顺序两个设计动机。
可能的追问与易错点。
「和 Redis 分布式锁比呢?」一句话版本:Redis 锁靠过期时间兜底——持有者挂了,等 TTL 到了锁自然释放,但 TTL 设短了可能误释放、设长了恢复慢;Zookeeper 锁靠会话生命周期兜底,持有者失联由会话过期机制判定,不用拍超时参数,代价是要维护一个 ZK 集群。
「生产里要手写这套流程吗?」不要。Apache Curator 这类官方生态客户端框架提供了现成的分布式锁实现(InterProcessMutex),全局同步、可重入。它的文档里有一条值得背下来的警告:客户端要监听连接状态——收到 SUSPENDED 时不能确定自己还持有锁,收到 LOST 则一定已经失去锁。网络抖动让会话「假死」时,手里的锁可能已经被判过期,醒来后第一件事是重新确认锁归属,而不是继续干活。
易错点主要有三:一是监听整个锁目录而不是前驱节点,惊群原样回归;二是忘记 watch 是一次性的——触发后即清除,醒来必须重新检查、重新注册;三是拿到锁之后长事务阻塞心跳,会话被误判过期,锁提前丢失,活儿还没干完锁就没了。
回到面试现场,建议的答题节奏:先给抢锁四步流程,再把「临时」「顺序」两个修饰词分别映射到死锁与惊群两个故障上,最后补一段与 Redis 锁的对比。模型更新发布、训练任务调度互斥这类 AI 工程里的分布式锁场景,选型分歧最终都落到「靠什么兜底」这同一个问题上。
你在生产里选分布式锁时,更看重哪一边的代价?欢迎聊聊。