资讯详情

分布式锁面试与实战:Redis、ZooKeeper、数据库方案对比与选型

📅 2026/10/10 20:58:46 | 华诺云谱 👁 阅读
分布式锁面试与实战:Redis、ZooKeeper、数据库方案对比与选型
分布式锁这个话题基本属于后端面试必考而且问法五花八门有时直接让你“手写一个分布式锁”有时给你一个业务场景问“这里要不要用锁”有时候让你比较 Redis 和 ZooKeeper 实现锁的差异。标题里的“每日面试题分享158”这个编号说明出题人应该是希望答出条理、答出层次感而不是上来就背一段 Redis SETNX 命令。我做后端开发这些年既在面试中被问过也在实际项目里踩过分布式锁的坑。说实话网上关于分布式锁的文章非常多但很多要么只讲 Redis 一种方案要么把 RedLock 吹得天花乱坠要么直接把数据库悲观锁搬出来容易把人带偏。这篇我就把自己在面试中会怎么答、在实际项目中会怎么选型完整梳理一遍。1. 先搞清楚单机锁为什么不能直接用到分布式环境1.1 从一段“看似没问题”的代码说起很多新手第一次接触分布式锁是看到线上出现超卖或者重复扣款于是查代码发现服务里加锁了用的还是synchronized或者ReentrantLock看起来没什么问题但问题就出在“看起来”上。举个例子某个订单服务部署了三台机器通过负载均衡对外提供服务。用户下单时请求可能被分发到任意一台机器上。代码里用synchronized保护了创建订单的逻辑这个锁在单台 JVM 内部是有效的能阻止同一台机器上的多个线程同时进入临界区但阻止不了另外两台机器上的线程同时进入。三台机器之间互相不知道对方有没有在跑同一段代码这就是单机锁失效的根本原因——锁的作用域只在进程内跨进程就管不到了。所以分布式锁本质上要解决的问题是让多个独立的进程甚至跨数据中心的进程对某个共享资源达成“互斥访问”的一致意见。1.2 正统分布式锁需要满足哪些条件面试的时候如果能主动说出分布式锁的五个必要条件基本上就能和普通背答案的候选人拉开差距。这五个条件也是后续所有方案对比的标尺互斥性任意时刻只能有一个客户端持有锁这是分布式锁最基本的要求。死锁规避持有锁的客户端崩溃或者网络异常锁必须能自动释放不能永久卡死后续请求。可重入性同一个客户端在持有锁的情况下再次尝试获取同一把锁应当能够成功避免自己把自己锁死。高性能加锁、解锁的耗时要低不能因为引入锁导致接口响应时间出现量级上的劣化。高可用提供锁能力的组件本身不能是单点一旦锁服务挂了业务也不能用。还有一个经常被忽略但实际非常关键的属性是锁的安全性也就是锁的持有者必须是“认领”的那个人释放锁的时候不能把别人持有的锁误删掉。这一点后面聊 Redis 实现的时候会详细展开。1.3 面试时怎么回答“为什么需要分布式锁”面试官问你“分布式锁一般都怎么实现”其实隐含了一个前置问题你知不知道在什么场景下才需要用到它我习惯用一个电商库存的例子来回答假设一个商品只有 10 件库存100 个用户同时下单。单体应用时代用synchronized锁住减库存的方法就行微服务化之后下单逻辑部署在多个实例上库存数据放在共享的数据库或缓存里每一个实例都必须先“抢到一把公共的锁”才能执行“检查库存、扣减库存、创建订单”这三个动作否则就会出现超过 10 件商品被卖出的情况。这个例子说完面试官基本就能确认你理解分布式锁的使用场景而不是单纯背概念。2. 三种主流实现方案的完整拆解2.1 基于数据库的实现最容易理解但最不推荐数据库方案是分布式锁最早期的形态思路非常直观利用数据库表的唯一索引来实现互斥。有一张lock表里面记录锁的名称和持有者信息尝试加锁就是往表里插入一条记录因为唯一索引的存在同一时刻只能有一个客户端插入成功。CREATE TABLE distributed_lock ( lock_key varchar(64) NOT NULL, holder varchar(64) NOT NULL, expire_time datetime NOT NULL, PRIMARY KEY (lock_key) ) ENGINEInnoDB;加锁逻辑INSERT INTO distributed_lock(lock_key, holder, expire_time) VALUES (order:1, hostA, DATE_ADD(NOW(), INTERVAL 30 SECOND));如果插入成功说明拿到锁插入报Duplicate key错误说明锁被其他客户端持有等待重试。解锁逻辑就删掉自己插入的那条记录DELETE FROM distributed_lock WHERE lock_key order:1 AND holder hostA;这里有两个细节必须注意。第一holder字段不能省否则客户端 A 持锁超时后客户端 B 抢到了锁A 此时执行删除会把 B 的锁误删。第二expire_time必须加防止持有者崩溃后锁永远不释放形成死锁。数据库方案最大的优点是实现简单、不用引入额外组件业务量小的内部系统完全够用。但它的问题也很致命数据库的并发能力有限一旦加锁请求量上来数据库连接和行锁竞争会成为新的瓶颈而且如果锁表所在的数据库是单机整个架构又引入了新的单点。我在实际项目中见过一个比较聪明的改良写法把纯INSERT改成INSERT ... ON DUPLICATE KEY UPDATE在冲突时判断expire_time是否已过期如果过期就立刻把锁“抢”过来避免等待轮询但这个方案需要配合应用层的自旋重试逻辑会复杂不少。用还是能用但确实属于“杀鸡用牛刀还未必顺手”的范畴。2.2 基于 Redis 的实现最主流也最容易踩坑Redis 方案是目前业界使用最广的分布式锁实现原因无非是 Redis 快、简单、集群部署普及率高。核心命令是SET key value NX EX没有花里胡哨的 Lua 脚本也能实现一个基础版本。SET lock:order:1 hostA NX EX 30这条命令的意思是只有当lock:order:1这个 key 不存在时才设置并且设置 30 秒过期时间。NX保证了互斥EX保证了自动过期一个命令同时搞定两个核心需求。释放锁的代码值得单独说。很多人直接写DEL lock:order:1这是典型的错误示范。如果客户端 A 持有的锁已经过期客户端 B 获取到了同一把锁A 此时执行DEL等于把 B 的锁删掉了后面 C 又能拿到锁互斥性瞬间崩溃。正确的做法是用 Lua 脚本先比对持有者标识再删除保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endRedis 官方把SET NX EX Lua 解锁称为“An (un)fair lock”意思就是这套方案能保证基本互斥但公平性不做保证。用 Redisson 这样的客户端时它会自动完成上述逻辑对业务代码几乎零侵入。// Redisson 伪代码 RLock lock redissonClient.getLock(lock:order:1); boolean locked lock.tryLock(0, 30, TimeUnit.SECONDS); if (locked) { try { // 处理业务 } finally { lock.unlock(); } }Redisson 内部做的核心增强有两个。第一个是看门狗机制。默认leaseTime为 30 秒时Redisson 会启动一个后台定时任务每 10 秒为这把锁续期一次把过期时间重置为 30 秒。这个机制解决了一个经典难题业务执行时间超过了锁的过期时间锁被自动释放另一个线程拿到锁进来两个线程同时跑临界区。但用过看门狗的人也会有疑虑如果业务真的卡死锁会一直被续期永远不会释放所以实际编码时还是要设定合理的业务超时兜底。第二个是可重入支持。Redisson 的锁 value 不仅存持有者标识还存了一个计数。同一个线程多次lock()时计数递增每次unlock()时递减减到 0 才真正删除 key。这个特性和 Java 的ReentrantLock是同一个思路。2.3 基于 ZooKeeper / etcd 的实现一致性优先的可靠方案ZooKeeper 实现分布式锁用的不是临时节点而是临时顺序节点 监听机制。思路是这样的在锁的根节点下创建一个临时顺序节点。判断自己创建的节点序号是不是当前最小的如果是说明拿到了锁。如果不是监听序号比自己小的前一个节点等它删除后再重新判断自己是否是最小节点。这里临时节点的意义在于如果客户端崩溃ZK 会自动删除与客户端会话绑定的临时节点不需要设置过期时间也就不会出现 Redis 方案里“业务没跑完锁却被过期释放”的问题。etcd 的实现思路类似但机制上是基于租约Lease和 revision 版本号。客户端创建一个租约将锁绑定在租约上租约到期后锁自动失效同时客户端通过心跳续约。多个客户端同时创建同一个 key 时只有 revision 最小的那个能拿到锁其他客户端 watch 这个 key 等待释放。这块写代码会比 Redis 方案长不少所以实际项目中很少裸写 ZK 或 etcd 的客户端库来做锁一般用封装好的组件。Apache Curator 提供的InterProcessMutex就是基于 ZK 的成熟实现etcd 生态里也有对应的锁服务比如 etcd 官方文档里基于 concurrency 包的例子。基于 ZK/etcd 的方案最大的优势是强一致性。锁的状态变更经过了共识算法不会出现 Redis 主从切换导致的锁丢失问题。代价则是性能不如 Redis一次加锁需要多轮网络交互延迟通常在几十毫秒级而 Redis 的 SETNX 通常在毫秒以内。3. 方案对决面试中怎么把“为什么选它”讲清楚3.1 一张表看清差异面试的时候如果只是把三个方案罗列一遍其实是及格水平。想拿高分得能给出清晰的选型依据。我通常用这样一张表格总结对比维度数据库方案Redis 方案ZooKeeper / etcd 方案实现成本最低直接用现有库低需引入 Redis高需引入独立组件性能最差受限于 DB最好毫秒级中等几十毫秒级自动释放机制靠过期时间靠过期时间看门狗续期临时节点 / 租约会话结束即释放死锁风险过期时间设置不当会锁死过期时间设置不当会提前释放极低会话模型天然规避可重入需要额外字段维护Redisson 已实现Curator 的 InterProcessMutex 已实现时钟影响无依赖本机时间时钟跳跃有风险无靠逻辑时钟典型场景内部管理系统、低频任务高并发、容忍极小概率锁失效对一致性要求极高的场景如分布式任务调度这张表背后其实反映出一个核心观点没有“最牛”的分布式锁方案只有“当前场景下最合适”的方案。3.2 热点追问Redis 主从切换真的会丢锁吗这是面试中我最喜欢追问的问题之一因为 70% 的候选人答不上来。背景是Redis 主从架构下数据从主节点异步复制到从节点。客户端 A 在主节点上成功加锁但主节点还没来得及把这条数据同步给从节点就挂了哨兵把从节点提升为新的主节点。此时客户端 B 向新主节点申请同一把锁因为数据没同步过来B 加锁成功。于是 A 和 B 同时认为自己持有锁互斥性被打破。针对这个问题Redis 官方提出了 RedLock 算法要求客户端向集群中大多数一般是 5 个独立节点中的 3 个以上同时加锁只有超过半数成功才算加锁成功。RedLock 在工业界的争议很大一部分人认为它并不能在真正意义上解决安全性问题反而增加了复杂度和延迟。我在面试中会给出一个工程取向的回答如果业务场景允许极低概率的并发冲突比如限流、幂等校验中多一次查询不至于产生严重后果那么 Redis 单机加锁完全够用如果场景绝对不能接受两个客户端同时持有锁比如资金类操作那么应该优先考虑 ZK/etcd 方案因为它的共识机制从底层保证了锁状态的一致性。这个回答的好处是诚实、有取舍、有判断力面试官通常会认可。4. 实战经验实现与使用中的六个关键坑4.1 坑一锁过期时间设置多少才合理这是分布式锁实战里最容易翻车的地方。过期时间设短了业务还没跑完锁先释放了造成并发穿透设长了持锁方宕机后其他客户端要等很久才能获得锁可用性降低。常规做法是设置一个保守值比如 30 秒同时配合 Redisson 的看门狗自动续期。如果项目没有用 Redisson而是自己基于 SETNX 实现那么单纯依赖一个固定过期时间是很危险的。我见过一个实际案例某定时任务在极端情况下执行超过 60 秒而锁过期时间恰好是 60 秒结果两个实例同时跑了同一批重活产生了大量重复数据。后来改成“过期时间 业务预估最大耗时 × 2 5 秒缓冲”并把任务内部拆小遇到长时间任务主动续期问题才解决。4.2 坑二解锁必须比对持有者标识前面已经反复强调过释放锁的 Lua 脚本必须校验持有者。这里再补充一个容易忽略的细节持有者标识不能只用一个 UUID 字符串。在微服务架构里同一个服务有多个实例同一实例又有多个线程锁标识最好是“实例 ID 线程 ID UUID”的组合确保唯一性防止两台不同实例的相同线程号互相解锁。4.3 坑三可重入锁在嵌套调用中必不可少业务开发时经常出现一个方法加了锁内部又调用另一个也加了同一把锁的方法。如果锁不支持重入第二次会把自己卡死或者因为等待锁超时抛异常。使用 Redisson 和 Curator 时这个坑基本不存在但如果是自己写 Redis SETNX 的极简实现就一定要在 value 里维护重入计数。4.4 坑四锁的粒度比你想的更值得关注分布式锁的 key 粒度直接影响系统吞吐量。如果所有订单请求都用同一个 keylock:order那么即使订单之间毫无关联也会被强行串行化性能大打折扣。更合理的做法是尽量把锁粒度降到业务对象维度。比如按订单号加锁lock:order:100001按用户维度加锁lock:user:9527。锁的粒度越细并发度越高但过细的粒度也会导致锁数量膨胀占用 Redis 内存。这个平衡需要在实际业务里反复调整并没有统一答案。4.5 坑五高并发下的羊群效应用 ZooKeeper 实现锁时如果 100 个客户端同时等待同一把锁简单方案是每个客户端都监听同一个节点锁释放时 100 个客户端全部被唤醒然后同时竞争这叫羊群效应。正确做法是只用监听前一个节点形成一条等待链锁释放时只有下一个节点被唤醒竞争范围缩小到最小。这个细节在面试中能答出来说明你真的看过实现源码。4.6 坑六业务逻辑中加锁的位置这一点已经不算技术坑更像设计坑。我见过不少同事把分布式锁加在 controller 层结果锁内还要调用远程接口响应时间被拉长锁一直被占用。我更推荐的做法是锁应该包裹真正的临界区而且是越窄越好。能用乐观锁如版本号 CAS解决的场景比如单纯更新一个状态字段就不要上分布式锁必须上锁的场景也要把网络 IO 尽量移出临界区。5. 面试回答的整体框架与话术参考单说知识点零散面试容易答得没条理。我总结了一个三段式回答框架可以覆盖大部分面试场景。第一段先讲使用场景把“为什么要用分布式锁”用一个业务例子说清楚。第二段列出常见方案并说明各自的原理和优劣这里可以直接使用上一节的对比表格逻辑。第三段抛出深度思考比如提到“Redis 主从切换丢锁”“RedLock 的争议”“ZK 与 Redis 方案的安全性差异”。模拟一下回答中的关键段落“我会根据业务的一致性要求来选型。如果是对一致性要求极高、不允许任何并发穿透的场景我会优先考虑基于 ZooKeeper 或 etcd 的实现因为临时节点和租约能做到会话级别的自动清理不存在过期时间设置不合理的问题。如果是高并发、追求低延迟的场景我会选择 Redis Redisson 的组合利用 SETNX 加 Lua 脚本解锁打开看门狗来自动续期。另外无论选择哪种方案我都会在业务侧做幂等兜底因为分布式环境下没有任何锁能做到绝对安全最终防线一定是业务本身。”这段话基本把知识储备、选型思路和工程经验都展示了。如果面试官不打断顺着这个思路往下讲整场回答会非常有说服力。6. 顺着这个题目还能延伸的两个面试话题如果时间充裕我会继续延伸两个相关方向因为分布式锁本质上只是分布式协同的一个子集。第一个是分布式幂等和分布式锁的关系。锁和幂等侧重点不同锁是为了避免“同时执行”幂等是为了容忍“重复执行后结果一致”。实际项目中我倾向于优先做幂等因为幂等是无锁设计扩展性比加锁好。例如使用数据库唯一索引、状态机前置校验或者 RedisSETNX做幂等标记都比锁更加轻量而且能规避锁的很多副作用。第二个是最终一致性和锁的关系。有些业务场景其实不需要强一致互斥只需要“最终只有一个成功”比如多个服务同时处理同一个消息我们可以让它们先抢一个 Redis 的 key抢到的处理、没抢到的直接丢弃这样比持锁等待更高效也更符合分布式系统“宁可重试不可阻塞”的设计哲学。把这两个话题延伸讲完不仅证明了分布式锁的知识掌握还能展示对整个分布式系统设计思维的深度面试观感会非常好。7. 日常开发中的最终建议聊到落地的层面说说我个人这些年反复验证过的几条建议算是给文章收个尾也希望真的能帮到在做架构选型的读者。如果你在维护一个中等规模的后端系统Redis 已经是基础组件Redisson 的分布式锁是默认选择没有必要为了“更可靠”去引入 ZK/etcd。引入一个新组件会给运维和监控带来额外成本而大部分业务场景根本触发不到主从切换丢锁这个极端事件。如果你在做支付、对账这类资金敏感业务分布式锁只应该作为最后一道闸门核心还得靠数据库的唯一约束和事务。纯靠 Redis 锁保证资金安全我是无论如何不敢这么设计的。另外无论用哪种方案一定要给锁加上监控。我在生产环境维护过一套分布式锁组件后来专门给每个锁 key 加了获取耗时、等待耗时、释放失败次数这几个指标一旦发现某个锁的平均等待时间异常飙高就立刻排查是否存在锁粒度过粗或者持锁时间过长的问题。没有监控的分布式锁就像没有仪表盘的飞机能飞但你不知道什么时候会出事。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑