多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

分布式锁选型与避坑指南:Redis、ZooKeeper、etcd深度对比

分布式锁选型与避坑指南:Redis、ZooKeeper、etcd深度对比 1. 上篇讲了什么这篇该重点读哪里如果你手边正同时开着 Redis、ZooKeeper 和 etcd 的文档再对照着读这篇文章说明你已经进入了分布式锁的正确状态。我在上篇把分布式锁最基础的东西讲透了包括它用来解决什么问题、数据库行锁怎么实现互斥、Redis 的set nx ex怎么用、ZooKeeper 临时节点为什么能自动释放还给了两段能跑的最小示例代码。这些是地基但只有地基是不够的。为什么这么说因为很多同学在读完上篇后把示例代码放到了生产环境然后回来找我锁能加上也能释放但到了线上就时不时出现重复执行、死等、锁不住。原因其实不难理解基础实现解决的是“这条代码路径上有没有锁”而生产环境要求的是“这把锁在故障、超时、网络抖动、GC 停顿和主从切换时还靠不靠谱”。所以这一篇我不打算再重复上篇的基础代码和入门概念而是集中回答下面这几个真正决定成败的问题Redis 锁在异常场景下的安全边界到底在哪里Redisson 的看门狗和 Lua 脚本是怎么规避误删和续期问题的ZooKeeper 锁为什么比 Redis 锁更接近强一致代价又是什么etcd 的租约模式在云原生环境下怎么落地以及脑裂、时钟跳跃、锁误删这三座大山分别用什么思路去拆。文章末尾我还会给出一份可以直接抄作业的选型表。阅读顺序上第 2 到第 4 章是三种主流实现的深入剖析适合想彻底搞懂方案细节的人第 5 章讲原理和争议做架构和运维的同学重点看第 6、7 章是工程实践写业务代码的可以多花时间第 8 章是排障速查我建议所有读者都至少扫一眼没准哪天就用得上第 9 章是面试与选型近期有面试或正在做技术评审的直接翻过去。2. Redis 分布式锁的三种实现路径2.1set nx ex只是入门别把它当生产级实现进过生产环境的同学大概率都走过这条路用set lock:key value nx ex 30加锁业务跑完再del lock:key释放。这套写法在单节点 Redis、没有并发歧义、也没有任何超时的情况下确实能工作。但它有三个很硬的伤。第一锁过期时间到了业务还没执行完另一个线程就能抢到同一把锁导致两个并行业务同时操作同一份数据第二持有锁的线程在删除锁时如果锁已经被别人续期或重新获取del会把别人的锁给误删掉第三主从架构下主库上的锁数据还没来得及同步到从库主库就宕机了从库被提升为主库后锁就丢了。这三个问题不是 Redis 本身罪大恶极而是你把它当成了数据库事务在用。分布式锁本质上是进程间协作的互斥设施它只能保证“大多数情况下大家不会同时动手”做不到绝对的物理隔离。理解这一点才能明白为什么后面要引入 Redisson 和 Lua 脚本也才能明白 Redlock 算法为什么会存在。这里我插一句特别实操的经验哪怕你最后没有引入 Redisson也务必把“校验锁归属 删除锁”这两步放进一个 Lua 脚本至少保证语义上的原子性。为什么因为在并发场景下get判断和del删除如果分开执行中间任何一个停顿都会导致误删。你永远无法预测 JVM 什么时候会在两条命令之间发生一次 GC 停顿而 GC 停顿恰恰是高并发下最容易被忽略的线程不安全时刻。2.2 Redisson 可重入锁的核心设计Redisson 是 Redis 生态里用得最广的分布式锁客户端它的核心优势是加锁、续期、释放都是通过 Lua 脚本原子完成的并且原生支持可重入。所谓可重入就是同一个线程可以重复获取同一把锁每获取一次计数器加 1释放一次减 1减到 0 才真正删除这个锁 key。它的加锁逻辑站在源码角度翻译成人话大概是这样的先检查锁 key 是否存在不存在就直接写入一个 HashHash 的 field 是当前线程的线程标识value 是重入次数同时设置过期时间并返回加锁成功存在呢再判断 Hash 里的 field 是不是当前线程如果是就对 value 加 1并刷新过期时间返回加锁成功否则返回失败。你品一下这个设计用一个 Hash 结构同时承载“锁持有者是谁”和“重入了几次”。这就天然解决了两件事一是可重入计数二是锁的归属校验。删除锁之前必须确认当前线程就是持有者否则不删这样就把上面说的锁误删问题在原理上堵死了。这里有个特别容易踩的坑我单独拎出来讲Redisson 默认加锁后的过期时间是 30 秒看门狗每 10 秒自动续期一次。但如果你在调tryLock时手动传了leaseTime看门狗就不会启动。这是很多人没注意到的分支逻辑——只有不传leaseTime时Redisson 才会按锁过期时间的三分之一周期做定时续期。一旦手动指定了租约续期机制就失效了锁到期后没人管业务要是没跑完锁就没了。所以用 Redisson 的时候要么清楚知道自己传了leaseTime的意义要么就老老实实用默认的看门狗续期。2.3 看门狗不是银弹它的代价也要评估看门狗本质上是一个后台调度任务加锁成功后它的定时任务每 10 秒检查一次锁是否还被当前线程持有如果持有就把锁的过期时间重置回 30 秒业务执行完释放锁时看门狗任务也会被取消。这套机制规避了“业务没跑完锁先过期”的问题确实解决了很多人的失眠问题。但看门狗不是免费的。第一个代价是进程宕机时锁只能等待自然过期。比如持有锁的服务被kill -9看门狗没了Redis 里的锁最多还剩下已经设置好的 30 秒这 30 秒内其他线程只能干等。第二个代价是续期请求本身会增加 Redis 的访问次数。我去年做线上压测时发现当系统里同时存在几百把长时间持有的锁时仅看门狗续期产生的请求就占到了 Redis 总 QPS 的百分之十几。后来我把不需要长任务的地方全部改成短锁、不续期才把这部分开销压下去。所以我的建议是看门狗该用就用但不要全局一套参数走天下。长任务、短任务分开配置锁的过期时间也不用一律 30 秒而是根据业务 P99 耗时设置一个基础值再决定要不要开续期。这样既保留了看门狗的优点又控制了它带来的额外压力和故障后的等待时间。3. ZooKeeper 锁羊群效应没你想象的严重3.1 临时顺序节点是怎么排队的ZooKeeper 实现分布式锁的核心思路用一句话概括就是“排队拿号”。所有想抢锁的客户端都在同一个锁目录下创建一个临时顺序节点ZooKeeper 会按创建顺序给节点编号。所有节点里序号最小的就是当前持锁者其他节点不直接去抢而是监听自己前一个节点的删除事件一旦前一个节点被删除自己就变成最小的节点从而获得锁。这个设计的精妙之处在于锁的生命周期和客户端会话绑定在了一起。客户端只要活得好好的ZooKeeper 就帮它维护临时节点客户端进程挂了会话断开临时节点会被自动删除锁也随之释放。整个过程不需要像 Redis 那样设置过期时间也不需要看门狗去续期从机制上就解决了“业务没跑完锁就没了”的难题。代价当然也存在。ZooKeeper 锁的延迟比 Redis 高吞吐也比 Redis 低因为创建节点、删除节点、监听变化都要走一遍 ZAB 协议多节点间的网络交互成本在那里摆着。而且 ZK 集群本身的可用性上限取决于多数节点一旦出现多数节点故障整个 ZooKeeper 会拒绝写入锁也就拿不到了。所以 ZK 锁适合对一致性要求高、但对吞吐不是极端敏感的核心场景比如分布式调度、配置变更、任务选主。3.2 羊群效应的触发条件和误解澄清羊群效应这个词我在面试里被问到的频率相当高。它的一般描述是一把锁释放之后有成百上千个客户端都在等待这把锁如果它们同时被唤醒同时再去抢锁就会造成巨大的瞬时流量和性能抖动。很多同学想当然地以为 ZK 锁会有严重的羊群效应其实这是把不同实现混在一起了。在临时顺序节点模式下每个等待的客户端只 watch 自己前一个节点所以锁释放时只会精确地通知一个节点其他节点完全不会被打扰羊群效应在原理上就被设计掉了。只有当你用临时非顺序节点、并且所有等待者都 watch 同一个锁节点时释放锁才会唤醒全部等待者那才是典型的羊群效应。所以面试的时候如果被问到“ZooKeeper 分布式锁有没有羊群效应”你最好先反问一句你说的是哪种实现如果对方问的是 Apache Curator 的InterProcessMutex答案就是可控因为它的实现正是顺序节点加只监听前一个节点。能给出这个层面的区分比单纯背概念要加分得多。3.3 Curator 锁的使用和三个隐蔽坑Apache Curator 是 ZooKeeper 客户端里对锁封装得比较完整的库InterProcessMutex提供了我们平常用的acquire、release、可重入、带超时获取等能力。现实项目里我们通常只需要几行代码就能把分布式互斥搭起来完全不比用 Redisson 费劲。但在 Curator 的使用里有三个坑是官方文档不会写在第一屏的。第一个是sessionTimeout的配置。这个值设得太短网络一抖动就会导致临时节点被误删锁被提前释放设得太长呢进程真正挂了之后其他线程要等很久才能拿到锁。一般建议在三秒到十秒之间具体看网络质量。第二个是锁目录别共享得太随意。不同业务如果都往同一个 ZK 节点下创建锁锁数量上去之后会让节点压力变大建议每个业务模块用独立的锁目录。第三个是跨进程可重入的问题。InterProcessMutex的可重入是 JVM 本地维度实现的在同一个进程内没问题但如果你把锁服务拆成多个容器又想在多个容器之间使用同一个锁标识完成跨 JVM 的重入依赖 JVM 内部的线程维度实现自然不能等价成跨进程语义。跨进程如果必须重入就要自己设计全局持有者标识来模拟。这三个坑都是我在实际项目里一个一个踩出来的。4. etcd 租约锁云原生里的新选择4.1 租约加 TTL 加续约的基本玩法etcd 在分布式锁上的设计和 ZK 有异曲同工之处但上手门槛更低。它引入了租约Lease的概念客户端先通过Grant接口创建一个租约租约带一个 TTL然后创建锁 key 时绑定这个租约接下来客户端需要周期性地对租约做KeepAlive也就是续约。一旦客户端挂了续约停止租约到期etcd 会自动删除绑定在该租约上的所有 key。这种模式和 ZooKeeper 的临时节点非常像但实现更直白你把一个 key 的生命周期绑定到一个租约上所有过期逻辑都由 Lease 统一管理代码读起来很清爽。加锁过程则通过Txn事务完成在一个事务里同时检查锁 key 是否已存在、写入锁 key 并携带租约 ID整个过程是原子的不需要 Lua 脚本。etcd 官方在clientv3/concurrency包里提供了更上层的封装Session负责租约管理和自动续约Mutex提供了Lock和Unlock你甚至可以用RWMutex做读写锁。如果你所在的技术栈已经使用 etcd 做服务发现或配置中心那引入分布式锁的成本几乎为零。4.2 etcd 锁在云原生环境里的优势为什么说 etcd 锁在云原生环境里特别合适因为很多基础组件本来就已经依赖 etcd。比如 Kubernetes 的选主机制、很多微服务框架的注册中心、Prometheus 的高可用部署都是基于 etcd 的协调能力。当你的系统里已经有了一套跑得不错的 etcd 集群再为分布式锁单独维护一套 Redis 或 ZK运维成本就显得很不划算。另外etcd 在网络分区下的表现比较干净。当客户端和 etcd 之间发生分区、暂时连不上时锁并不会永久阻塞在那里租约到期后锁会自动释放这对调度类任务而言非常友好因为这类任务最怕的就是“锁默默烂在某台机器上”。从一致性角度看etcd 基于 Raft 协议写请求必须经过多数节点确认它提供的是强一致语义这是 Redis 单节点锁给不了的。不过etcd 锁也不是完美的。它的性能和延迟介于 Redis 和 ZK 之间而且如果你对 etcd 本身不熟租约滥建、不主动释放 Session也可能造成 etcd 资源泄漏。我建议在使用时统一收集 Session 的生命周期由框架帮你续约和释放同时定期监控 etcd 的租约数量。5. 分布式锁的三座大山脑裂、时钟与锁泄漏5.1 脑裂到底会让锁丢在哪里脑裂在分布式系统里指的是网络分区导致一个集群被拆成几个互不通信的小团体每个小团体都认为自己是“活着的那个”。在 Redis 哨兵或集群模式下如果主库与从库发生分区哨兵可能在原主库还存活的情况下把从库提升为新主库而新主库上并没有完整同步旧主库上的锁 key。这时候旧主库上拿锁的客户端还自以为持有锁新主库上另一个客户端也成功拿到了锁就出现了两把同时有效的锁。缓解脑裂的办法常见的有三个。第一个是设置 Redis 的min-replicas-to-write让主库在从库数量不足时直接拒绝写入这样锁 key 丢失的概率会降低。第二个是采用 Redlock 多节点算法把请求分散到多个独立节点只有大多数节点同意才算加锁成功避免单个节点故障导致全局失锁。第三个是干脆换用 ZooKeeper 或 etcd 这种强一致协调组件因为它们的多数派协议天然要求多数节点存活才能提交写入。这三个办法没有绝对优劣。如果你对锁丢失的容忍度极低那 Redis 方案无论怎么调参都不如直接换存储如果只是希望概率从 1% 降低到 0.01%那调参数、加 Redlock 就足够了。把这条边界想清楚比盲目上高强度方案更重要。5.2 Redlock 算法的争议到底争什么Redlock 的思路是准备 5 个完全独立的 Redis 实例加锁时对这 5 个实例同时发送set nx ex请求只要其中 3 个以上返回成功并且总耗时小于锁的有效期就认为加锁成功。它把单点故障的容错转化为多数派问题思路本身没问题。但这个算法有非常有名的争论。分布式系统专家 Martin Kleppmann 写过一篇分析文章提出 Redlock 在客户端 GC 暂停和系统时钟跳跃时依然不够安全。举个例子线程 A 拿到锁后JVM 发生了一次长时间的 Full GCA 被暂停了 20 秒这期间锁到期了线程 B 拿到了锁开始写数据A 的 GC 结束后醒过来也继续写数据此时两个线程都在写。Redlock 解决不了这类问题因为问题不在锁的存储端而在客户端进程的主观时间里。Redis 作者 antirez 则回应说分布式锁本来就是一种尽力而为的互斥机制如果你需要的是物理意义上绝对安全的互斥就不要用 Redis应该用 ZooKeeper 或 etcd。这场争论到今天也没有统一答案但对我而言它的价值在于让我接受了一个更平实的结论没有一种分布式锁是绝对安全的只有适合当前场景的锁。你不能拿库存扣减的锁去跟银行转账的锁要求同等强一致。5.3 时钟跳跃为什么比想象的更致命时钟跳跃这个话题看起来学术实际上在生产环境里会真实咬人。Redis 判断 key 是否过期靠的是服务器本地时间。如果 Redis 服务器的系统时钟被 NTP 往回调了 5 秒一把原本还剩 10 秒过期的锁可能被系统认为是还剩 15 秒过期如果时钟往前跳了 5 秒锁又会提前 5 秒过期。前者让等待方白白多等后者更危险锁提前释放持锁方还没跑完别的线程已经抢到了。ZooKeeper 的会话超时和 etcd 的租约续期也有类似的时间依赖只是没有 Redis 过期时间来得那么直接。我在生产环境里见过几次诡异的“锁随机失效”最后定位到根因都是 NTP 跳变。从那以后我处理所有涉及锁的 Redis 实例时都会把 NTP 配置成slew模式而不是step模式也就是让时钟慢慢追赶而不是跳变同时对 Redis 服务器的时钟偏移增加监控。这个经验很多资料里不写但对稳定性影响极大。6. 高并发下的锁设计与性能优化6.1 大锁拆小锁分段锁的实际玩法分布式锁最常见的性能瓶颈不是 Redis 本身的吞吐而是同一把锁 key 上的并发争抢。当一个 SKU 的秒杀库存、一个热门账户的余额、一个热门主播的状态都集中在同一个 lock key 上时所有请求都会在这个 key 上排队Redis 单线程的处理能力再高也架不住锁内业务串行。分段锁的思路就是让热点 key 不再是唯一。比如库存扣减把一个 SKU 的库存拆成 10 个分片每个分片对应一个独立的锁 key比如stock:{skuId}:{shard}。请求进来时先按用户 ID 或请求 ID 哈希到一个分片只去抢对应分片的锁。这样并发能力理论上能提升接近 10 倍。代价是查询总库存时需要聚合 10 个分片的剩余量并且每个分片内的超卖判断要自己做逻辑比单 key 稍微复杂一点。但分段锁不是所有场景都适用。如果业务本身对资源就是强串行比如全局唯一订单号生成你没法拆成多个分片同时生成因为唯一性必须全局保证。这种时候就别想着拆锁了应该想办法缩短锁内的操作时间把数据库更新、通知发送、日志记录全部移到锁外让锁只保护“判断 更新”那一小段。6.2 可重入锁的实现与跨 JVM 的思维转换可重入问题在业务代码里比想象中常见。你写了一个加锁的公共方法方法内部又调用了另一个加同样锁的方法如果没有可重入机制第二次加锁会直接失败形成自己等自己的死锁。Redisson 和 Curator 都原生支持可重入这是它们的加分项。但如果你手写 Lock就得自己设计重入计数。我见过有人用手写方案处理重入把计数放在 ThreadLocal进程重启后计数丢失锁的语义就乱了也有人把计数放在 Redis 远端每次重入都多一次网络请求性能受损。我的建议很简单能用 Redisson、Curator 这种成熟库就绝不自己实现可重入。成熟库不仅是功能完整的问题更重要的是它们把异常路径和边界情况都处理过了省下的是你排查线上问题的时间。还有一个常被忽略的跨 JVM 问题分布式锁的持有者究竟是谁。在单 JVM 里锁持有者是线程 ID在多 JVM 里锁持有者必须是“进程 ID 线程 ID”这类全局唯一标识否则两个进程里恰好有相同线程号的线程就可能出现归属判断错误。很多半吊子教程里给的setnx示例value 直接写线程 ID这在多实例部署下就是错误示范。6.3 锁超时时间到底怎么定锁的超时时间是我在代码评审里最喜欢问的问题。很多人的第一反应是“设成 30 秒”问他为什么答不上来。其实这个参数有明确的设计目的它是一个兜底安全网用来保证持有锁的进程意外挂掉之后锁能在一段时间内被释放而不是永远卡死。太短的问题很清楚业务正常耗时稍微长一点就提前超时后面的人抢到锁两个并发任务开始重叠。太长的问题不太直观进程挂了之后其他线程等待锁的时间被无意义拉长故障恢复本来就慢锁还继续占着 10 分钟系统相当于停摆 10 分钟。正确做法是先压测出业务执行时间的 P99再留出 50% 到 100% 的冗余。比如 P99 是 5 秒超时时间设 10 秒到 15 秒比较合理。如果业务耗时很不稳定那就配合看门狗做自动续期让长任务主动续命而不是赌超时时间足够覆盖一切。7. 分布式锁使用场景与业务边界7.1 哪些业务真的值得上分布式锁分布式锁的使用场景面试常问的也就那么几个秒杀场景下的库存扣减、多节点定时任务调度、消息消费的幂等控制、以及多服务之间的资源统一管理。但我这些年更深的体会是很多场景其实不需要分布式锁。你能用数据库乐观锁解决的问题能用本地内存队列排队的尽量不要引入额外的协调组件。真正值得上分布式锁的场景必须同时满足三个条件多个进程需要操作同一个资源、资源本身无法通过数据库单一约束解决、并且对互斥的实时性要求比较高。比如优惠券发放多个服务同时从一个 Redis 队列里领券如果不加锁两个服务可能同时领到同一条记录如果用数据库行锁又会影响连接池性能。这种情况下一个简单的分布式锁 keycoupon:issue:{batch}锁内做连续领取锁外做业务处理就是最合理的解法。7.2 锁 key 的命名规范和粒度边界锁 key 的命名是团队工程素养的直接体现。我看到过不少事故根子最后都出在锁 key 没定规矩上。名字太宽泛比如直接用lock:order不同业务会互相锁名字太细比如把用户 ID 和订单 ID 拼接错了两个本来要互斥的操作又完全不互斥。所以锁 key 必须有一个强制的约定我现在的团队用的是一个通用模式业务域:资源类型:资源ID比如trade:order:20240101、stock:sku:10001所有涉及这个资源操作的服务统统走同一个 key。锁的粒度还要和业务操作的边界保持一致。如果你要同时操作一个订单的支付状态和库存那把锁的粒度设计到订单维度是合理的如果只操作订单里的一个子项锁却加在客户维度那就是过度加锁会把完全不相关的请求全部串行化。粒度设计的验证方法很简单问自己一个问题如果不加锁两个并发请求最坏会发生什么能答出这个问题粒度就自然浮现了。7.3 锁释放了但业务没完成怎么办这是分布式锁的经典边界问题线程 A 拿到锁处理完业务并释放锁但在 A 的释放动作生效之前线程 B 已经拿到了同一把锁开始处理同一条业务数据。如果业务操作没有幂等性兜底重复处理就发生了。我处理过很多“锁拿对了但数据还是错了”的 case最后证明锁本身没问题而是幂等没做。所以我在方案设计上有个铁律分布式锁不能替代幂等控制。锁解决的是并发时刻的串行但无法保证 A 和 B 之间的操作顺序一定按你预期幂等表、数据库唯一约束、版本号才是最终防线。比如消息消费者先查幂等表不存在才加锁处理处理完再插入幂等记录这样即使锁释放后有并发也能被唯一索引兜住。8. 常见问题与故障排查实录8.1 锁过期业务没执行完怎么办现象日志里加锁成功但业务执行时间超过锁过期时间日志出现两份相同的处理记录。原因通常是两类一类是锁过期时间设置得太短另一类是用了看门狗但写业务代码时手动传了leaseTime把续期机制覆盖掉了。解决办法分两步第一步检查代码确认没有传leaseTime第二步把过期时间结合业务 P99 重新评估。我见过最典型的现场是一个数据导入任务平均耗时二十秒锁过期时间却只有十秒业务方为了省事没有开看门狗结果每次大导入都会出现双跑。后来我直接把这类任务改成短任务分批每批三秒以内锁过期时间设为五秒问题彻底消失。数据和现象先对齐再下结论是排障的第一步。8.2 锁误删删掉了别人的锁现象释放锁的时候把另一个线程刚拿到的锁删掉了导致对方失去互斥保护。原因就是校验归属和删除锁不是原子操作或者校验用的 value 没有带上全局唯一的持有者标识。解决办法是统一改用 Lua 脚本完成“比较 value 删除 key”并且保证 value 用全局唯一标识生成比如进程 ID 线程 ID UUID 拼接。只要你用 Redisson 的RLock这个问题基本不会再出现。如果已经踩了这个坑临时恢复手段是把锁 key 的删除操作改成耗时更短、校验更严格的方式但长期方案一定是统一收口到标准库实现不做手工del。我在代码评审里看到手写setnx加get校验的都会直接打回去不是因为不能用而是因为风险完全不可控。8.3 锁等待时间很长但锁并没有死锁现象多个线程抢同一把锁其中一个线程拿锁后长时间不释放其他线程全部阻塞。首先看是不是业务在锁内干了耗时的操作比如 Redis 调用、远程接口、数据库批量更新其次看持锁线程是否发生了 GC 停顿JVM 的 Full GC 时间有时能到几秒加锁代码前后看起来没卡其实已经停顿了很久。定位手段很直接给加锁代码打上持锁线程的堆栈和耗时日志或者用 Arthas 的线程命令看一眼持锁线程。这类问题最容易出现在定时任务和批处理刚好重叠的时间窗口。我给一个客户排查过每天凌晨三点的数据清洗任务会持锁十五分钟而那段时间正好有另一个报表任务在等同一把锁。双方都没错纯粹是锁粒度设计不合理。后来把清洗任务拆到凌晨四点报表任务改走只读副本锁冲突自然消失。8.4 常见问题与排障速查表现象根因快速解决长期方案锁过期但业务还在跑过期时间设置过短 / 手传 leaseTime暂时调大过期时间压测 P99 后设置合理超时配合看门狗续期误删他人锁先 get 后 del中间停顿改用 Lua 脚本原子删除统一换 Redisson两个线程同时拿到锁主从切换导致锁丢失 / 时钟跳变开启 min-replicas校准 NTP核心资源改用 ZooKeeper / etcd锁等待时间异常长锁内有慢操作 / 进程 GC / 看门狗失效输出持锁线程堆栈缩小锁内范围、使用带超时的 tryLockRedis 连接池满大量线程抢锁失败重试频繁调整连接池参数分段锁降低抢锁频率、加锁失败快速返回9. 面试高能回答与分布式锁选型指南9.1 面试官问分布式锁他们到底想听到什么面试官问“讲讲你用过哪些分布式锁”很多人第一反应是背定义。其实他们真正想听的是你有没有在复杂环境下理解过这把锁的安全边界。高分回答的结构我总结过基本是四步。第一步先说为什么需要锁用具体的业务场景引出互斥诉求第二步说出三种主流实现Redis、ZooKeeper、etcd并对每种讲清楚原理和适用边界第三步重点讲你在中间踩过的坑比如锁误删、看门狗、脑裂、时钟跳动第四步给出选型和对应的监控方案。举个例子如果被问到 Redis 锁你可以顺着“setnx expire 的不足 → Lua 脚本保证原子性 → Redisson 可重入和看门狗 → Redlock 争议 → 最终选型要看业务容忍度”这么一条链路往下讲。这套链路下来既展示了知识也展示了工程判断力比单纯背出“Redis 锁基于 setnx”要高级得多。9.2 Redis、ZooKeeper、etcd 对照选型表维度RedisRedissonZooKeeperCuratoretcdconcurrency一致性最终一致存在锁丢失窗口强一致ZAB强一致Raft锁过期过期时间 看门狗会话绑定临时节点租约 TTL 续约性能极高微秒级中等比 Redis 差中等介于两者之间故障安全性依赖节点数、脑裂参数多数节点存活才可用多数节点存活才可用运维成本低复用现有 Redis 集群需要额外维护 ZK 集群复用 etcd 即可典型场景秒杀、库存、缓存互斥调度、任务选主、强一致互斥云原生、配置中心、注册中心9.3 我的选型建议和几个底线在这三类方案里做选择我的经验可以压缩成三句话。业务里 80% 的分布式锁场景Redis 就够了。即便它存在极小概率的锁丢失只要你用 Redisson、开启持久化、做好监控对大部分业务来说收益远大于风险。但如果你的场景里有“钱”、“账”、“唯一性”这些字眼别犹豫直接用 ZooKeeper 或 etcd宁可牺牲一点性能也不接受锁的语义打折。第三如果系统已经在用 etcd那优先使用 etcd 的concurrency包省心省力。我心里还有个不成文的底线任何一把分布式锁都必须配上三件套——加锁和解锁日志、锁等待时长与持有时长监控、锁释放后的幂等兜底。这三件事能让你在后面锁出问题时少通宵几个晚上也能在架构评审会上把每一个为什么说得明明白白。这篇写下来的核心目的就是让你既懂原理也知道工程上怎么落。我把坑和争议都摊开讲了希望你下一次加锁、选型、面试时能把“分布式锁”这四个字讲出自己的厚度。
返回列表