
面试被问分布式锁我第一反应其实是有点懵的。倒不是不会而是这个问题太经典了经典到每个面试官都觉得自己能问出点新花样经典到每个候选人八股文都背得滚瓜烂熟。但背八股和真正理解分布式锁是两码事。我见过太多人张口就是“SETNX加EXPIRERedisson的RedLock”结果追问一个“如果Redis主节点挂了怎么办”就卡壳。也见过真在线上把分布式锁用出事故的人压根没搞懂锁的粒度该多小、续期机制怎么设计。这篇文章我不想给你整理一份死记硬背的答案表而是想从面试官的视角拆一下这个问题他到底在考你什么你该怎么组织自己的回答体系以及回答里哪些地方一旦深入就特别容易露馅。说明这篇文章主要以Java技术栈 Redis/ZooKeeper/etcd为背景展开但分布式锁的核心原理跟语言无关其他语言的同学同样可以参考。1. 面试官抛出这个问题的真实意图考察的从来不是“怎么实现”这一个点很多人以为面试官问“怎么实现分布式锁”是在考API记忆能力。你只要背出来SETNX、EXPIRE、Redisson就完事了。大错特错。这个问题在面试官的题库里属于典型的“深水区问题”表面上问的是实现实际上他在通过这个问题同时考察你至少四个层面的能力。第一层是基础功。你知不知道分布式锁的基本要求是什么——互斥性、可重入、防死锁、高性能、高可用这些概念是否张口就来还是需要挤牙膏式地被提示。基础功不扎实的人往往只提“互斥”一点说明他对分布式环境下锁的设计维度没有完整认知。第二层是方案对比能力。市面上主流的分布式锁方案有数据库悲观锁、数据库乐观锁、Redis、ZooKeeper、etcd每一个背后都有不同的CAP取舍。面试官想听的不是你只精通一个方案而是你能不能横向对比说出每个方案在什么场景下选它是合理的。这一层考的是技术视野。第三层是源码深度。拿Redis方案举例“用SETNX加一个EXPIRE”是最基础的做法但如果你只看过这个那说明你的知识停留在博客层面。真正加分的是你能说出Redisson的看门狗续期机制、Lua脚本释放锁的原子性、以及为什么从Redis 2.6.12开始官方推荐用SET命令合并SETNX和EXPIRE。这一层考的是你是否真正读过源码、看过官方文档而不仅仅是看过二手博客。第四层是实战经验。线上场景中分布式锁会遇到什么bug锁的粒度怎么设计锁超时时间设多少合适持锁线程卡顿导致锁提前过期怎么办主从切换导致锁丢失怎么办这些才是区分“背八股的人”和“真正做过事的人”的分水岭。面试官在追问环节的所有问题几乎都围绕这一层展开。所以你在面试时回答这个问题的策略不应该是一口气把所有细节倒出来而是按“总—分—分—深”的结构。先给出分布式锁的四大核心要求再说明主流的三个实现方案及选型逻辑然后选定一个主方案展开实现细节最后主动抛出你踩过的坑和思考。这样的回答方式本身就已经和只会背八股的候选人拉开了差距。2. 数据库版本的分布式锁最朴素的方案为什么进不了生产环境面试官问到分布式锁很多时候会先让你说“你用过哪些方案”。这时候我的建议是从最简单的数据库方案开始讲起然后再过渡到Redis这样能体现出你对演进过程的理解。数据库实现分布式锁最常见的是两种思路。第一种是基于悲观锁也就是利用SELECT ... FOR UPDATE。当你对一个事务内的行记录加锁后其他事务再对该行做FOR UPDATE查询就会阻塞直到锁释放。第二种是基于乐观锁利用唯一索引或者版本号机制。比如建一张锁表锁名称加唯一索引获取锁就是往表里插入一条记录释放锁就是删除这条记录。多个节点同时插同一条记录时只有一个节点能插入成功其他的会走唯一索引冲突异常。我来说一下唯一索引插入方案的实现细节因为它曾经是个“看起来很美”的方案CREATE TABLE distributed_lock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, lock_name VARCHAR(64) NOT NULL, owner_id VARCHAR(64) NOT NULL, expire_time DATETIME NOT NULL, UNIQUE KEY uk_lock_name (lock_name) );获取锁的执行逻辑是尝试插入一条记录如果插入成功即获得锁。释放锁时需要通过owner_id来校验防止锁被其他线程误释放比如A线程的锁过期了B线程获取了锁此时A线程来释放锁就不应该生效。删除语句必须带上owner_id条件DELETE FROM distributed_lock WHERE lock_name ? AND owner_id ?;还要考虑锁过期的问题。如果持有锁的节点突然崩溃记录不会被自动删除其他节点就永远拿不到锁。于是你需要一个定时任务定期扫描数据库中超过expire_time的记录并清理掉这又引入了定时任务的间隔时差、时钟一致性等一系列问题。这套方案最大的问题在于性能。每一次加锁解锁都是一次数据库事务涉及网络IO、SQL解析、事务提交、行锁竞争。在低并发场景下勉强能撑住一旦并发量上来数据库很容易成为瓶颈。而且MySQL的FOR UPDATE在RR隔离级别下如果索引设计得不合理可能会升级为表锁那并发能力就直接被打到地板了。面试官如果继续追问“为什么不用数据库方案”你可以从这个角度回答数据库分布式锁不是不能用而是它的性能天花板比较低且在锁自动过期、可靠释放这两个关键问题上都需要额外的调度机制来兜底引入了更多的复杂性和不可控因素。所以在高并发场景下主流方案基本都选择了Redis或者etcd这类专门的中间件。3. Redis分布式锁从SETNX到Lua脚本的演化过程比答案本身更重要Redis分布式锁是面试中的绝对核心。几乎每个面试官都会让你详细描述Redis分布式锁的实现但这个问题的讨巧之处在于你可以用“版本演进”的方式来讲让面试官看到你不仅知道最终结论还清楚每一步演进的动机。第一版SETNX EXPIRE最开始的做法是用SETNX key value命令这个命令只有在key不存在时才会设置成功返回1表示拿到锁返回0表示拿锁失败。然后通过EXPIRE key seconds给锁加一个过期时间防止持有锁的进程崩溃后死锁。这个版本有一个经典的坑不是原子的。如果SETNX执行成功后进程在EXPIRE执行前崩溃了锁就永远不会过期其他节点全部阻塞。面试时你自己主动说出这个坑比面试官追问“所以问题是什么”要加分得多。第二版SET命令的扩展参数Redis从2.6.12版本开始官方推荐用一条命令完成加锁操作解决了原子性问题SET lock_key unique_value NX PX 30000这条命令的含义是只有当lock_key不存在时才设置这个key值设置为unique_value过期时间为30000毫秒。NX参数保证了互斥性PX参数保证了自动过期整条命令是原子的。到了这一步加锁算是有了一个相对靠谱的版本。但紧接着第二个坑就出现了——解锁操作。第三版Lua脚本解锁很多人解锁时直接DEL lock_key这也是个有问题的写法。如果A线程的锁已经因为超时被自动释放了此时B线程获取到了同一个锁A线程如果直接执行DEL就会把B线程的锁误删掉。这就违反了分布式锁的基本要求——锁只能被持有者释放。正确的做法是校验value再执行删除。而校验和删除这两步必须保证原子性否则在判断value一致之后、执行DEL之前锁依然存在被误删的风险。解法是用Lua脚本Redis服务端会原子性地执行整个脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段脚本先获取key的value与传入的unique_value进行比较相等才删除否则返回失败。通过比较value确保了只有锁的持有者才能释放锁。面试时如果你能顺手把这个Lua脚本写出来就已经说明你真的写过分布式锁而不是停留在“SETNX加EXPIRE”的博客认知阶段。Redisson的看门狗机制为什么需要续期到了这个版本Redis分布式锁的基础模型已经完整了。但如果你知道Redisson的看门狗机制就能把回答拔高一个层档。看门狗解决的核心问题是锁的过期时间设置多长合适如果设置得太短业务没执行完锁就过期了另一个线程在同一时间拿到锁两个线程同时进入临界区分布式锁互斥性的保证就失效了。如果设置得太长一旦持有锁的节点崩溃其他节点要等待很久才能恢复。Redisson的默认做法是锁的过期时间设置为30秒但加锁后启动一个定时任务每隔10秒默认是锁剩余时间的1/3就去刷新一次过期时间。只要线程还活着锁就永远不会过期。这背后的原理其实很像我们平时写代码时的“心跳检测”——通过持续的心跳向Redis声明“我还活着请不要释放我的锁”。实际在Ression中加锁成功后看门狗会去检查Redis中锁key的剩余存活时间如果剩余时间接近过期就执行一次PEXPIRE刷新。这个机制保证了业务执行时长的不确定性不会导致锁提前失效。主从架构下的锁丢失问题为什么会有红锁争议面试到这里面试官大概率会抛出那个杀伤力很大的问题如果Redis使用主从架构A线程在主节点获取了锁但主节点还没来得及把数据同步到从节点就宕机了从节点被提升为新的主节点。此时B线程去新的主节点获取同一把锁因为锁数据没有同步过来B线程能成功获取锁。两个线程同时持锁互斥性被打破。怎么办这就是为什么Redis官方在分布式锁场景下推出了RedLock算法。RedLock的核心思想是部署多个独立的Redis节点通常建议5个加锁时向所有节点发送加锁请求如果超过半数节点N/21加锁成功并且总耗时小于锁的过期时间就认为加锁成功。释放锁时向所有节点发送解锁请求。RedLock在理论上看起来合理但在工程界争议极大。最大的争议点是RedLock假设各个Redis节点之间是相互独立的不存在任何同步关系。如果这个假设不成立RedLock的安全性就得不到保证。同时RedLock引入了额外的部署成本和运维负担在大多数业务场景下显得过于“重”。我在实际工程中很少推荐纯RedLock。更常见的做法是接受“极端主从切换场景下锁可能失效”这一事实在业务层面做兜底比如幂等机制、数据库唯一约束兜底。Redis分布式锁的核心价值在于高性能它对应的应该是那些“允许极小概率下锁失效但绝不能因为加锁而拖垮性能”的场景。这一点你自己心里要清楚。4. 锁释放、重入、续期、等待分布式锁的四大进阶设计点面试官很少会在Redis加锁解锁的主干逻辑上止步他一定会往深处挖。我在回答完Lua脚本之后几乎每次都会被追问下面这四个进阶设计点。这些问题看起来独立其实是分布式锁在真实生产环境里最容易踩坑的地方。4.1 锁的可重入性为什么一个线程要多次获取同一把锁可重入的意思是说同一个线程在已经持有锁的情况下可以再次获取同一把锁而不会被阻塞。这在业务代码中其实很常见比如一个方法内部调用了另一个方法两个方法都加了同一把分布式锁。如果锁本身不支持可重入这时就会发生死锁。因为外层方法还没执行完锁还没释放内层方法又想获取同一把锁就永远等不到。Redisson对可重入的实现方式很有意思锁的value不再是一个简单的随机字符串而是采用hash结构field是持有锁的线程标识value是重入的计数。每次重入时对该计数器加1每次释放时减1减到0才真正删除锁。在回答这个问题时建议大家顺便提一点分布式锁的可重入性本质上和synchronized、ReentrantLock的可重入是同一个设计思想。唯一的区别是分布式环境下你需要自己通过线程标识计数来实现不能依赖JVM层面的锁状态。这样回答能把面试官从“Redis命令”拉到“并发设计”的高度。4.2 锁的续期过期时间设太短是事故设太长也是事故这个问题我在上一节提过Redisson的看门狗已经做了自动续期。但面试官可能会追问如果不用Redisson自己怎么实现续期一种简单可靠的方案是用定时任务。加锁时启动一个单独的调度线程每隔一定间隔比如锁过期时间的1/3执行一次续期命令刷新过期时间。在释放锁时要将这个调度线程一并关闭避免续期一个已经释放的锁。这里有个细节容易被忽略续期的前提是你要确认当前的锁仍然属于当前线程。否则续期操作会把其他线程的锁的过期时间也刷新了。我见过一个生产事故某团队自定义实现了分布式锁加锁时起了定时器续期忘了在不持有锁时停止续期。结果A线程的锁已过期被B线程获取A线程的定时器还在继续给同一个key续期B线程的锁永远无法过期整个系统的锁基本等于形同虚设只能重启服务才恢复。所以面试时你可以主动说出这个坑面试官的反应通常都会不错因为它不仅能体现你理解了续期的原理还说明你踩过真实的坑。4.3 锁的等待与超时获取不到锁时你到底该怎么办很多人实现分布式锁的时候会陷入一个误区拿不到锁就在代码里自旋重试直到拿到锁为止。这样做的结果是一旦Redis或锁本身出现问题整个线程就卡死在自旋上连接池被打满系统雪崩。正确的做法是设置合理的等待超时时间。获取锁失败时先尝试等待一段时间如果等待超过阈值仍然拿不到锁就直接返回失败或执行降级逻辑。Redisson的tryLock方法就是这种思路的实现你可以指定等待时间、锁的持有时间、时间单位三个参数。这里有一个值得展开的设计思考业务拿到锁之后如果长时间拿不到锁应该让请求快速失败并提示用户稍后重试还是让请求排队等待这完全取决于业务场景。如果是“库存扣减”这类高一致性场景排队等待是可以接受的如果是“用户领取优惠券”这类体验敏感场景快速失败返回提示可能更好。分布式锁的等待策略本质上是对用户体验和数据一致性之间权衡的结果。4.4 锁的粒度不要把整张表锁住再去做一行数据的操作分布式锁最容易犯的另一个错误是锁的粒度设计得过大。比如在秒杀场景下很多人会用“商品ID库存操作”这一整把锁来锁住所有库存变更但其实只需要锁住“该商品的库存扣减”就够了。如果把锁设计成全局唯一的那整个系统的并发能力会被锁直接限制住和单机synchronized没有任何区别。锁的粒度设计原则是在保证数据一致性的前提下锁的覆盖范围越小越好。一个常见的思路是在锁的key中带上业务维度比如订单维度、用户维度、商品维度。每一个维度的锁只影响同一维度下的操作不同维度之间的请求完全互不干扰。这个思路本质上就是在分布式环境下对“临界区”做更精细的划分让并发能力接近理论上限。5. ZooKeeper与etcd的分布式锁CP路线的正确打开方式聊完了Redis方案面试官通常还会有一段“方案对比”的考察。他会问除了Redis你还知道哪些分布式锁实现方式各有什么优缺点这时候掏出ZooKeeper和etcd就能展示你的知识广度。5.1 ZooKeeper临时顺序节点实现分布式锁的思路ZooKeeper实现分布式锁的经典方案是使用临时顺序节点。整体思路是所有想要获取锁的线程在同一个持久节点下创建临时顺序子节点。每个线程创建完节点后检查自己创建的节点是不是当前所有子节点中序号最小的如果是就认为自己获取了锁成功。如果不是就监听自己前一个节点的删除事件等待前一个节点释放锁。这里有几个关键点值得在面试时展开。第一临时节点的特性是客户端断开连接或会话超时时节点会被自动删除这就天然解决了“持有锁的节点崩溃导致死锁”的问题不需要像Redis那样依赖过期时间。第二监听前一个节点的删除事件让线程按顺序依次获取锁天然实现了公平性。这个公平性比Redis方案好很多——Redis的锁是没有公平性可言的谁抢到算谁的。ZooKeeper方案的缺点是性能不如Redis。因为每次加锁解锁都涉及ZooKeeper的写操作还要进行节点监听整个过程的网络交互次数远多于Redis。在超高并发场景下ZooKeeper会成为性能瓶颈。5.2 etcdlease CAS 的组合拳etcd在实现分布式锁的方式上比ZooKeeper更现代化一些。它的基本思路是用lease租约来实现自动过期用CASCompare-And-Swap来保证互斥。加锁时向etcd写入一个key绑定一个租约同时使用事务判断key是否已存在如果不存在则写入成功视为获取锁。等等。有一点很重要的是和Redis的过期时间类似etcd的lease也存在时间一到就自动失效的机制因此需要KeepAlive续约。但etcd本身是Raft协议保证一致性的不存在Redis主从切换时锁丢失的问题。这也是为什么很多对一致性要求更高的业务会愿意牺牲一些性能去选择etcd。面试时如果能把ZooKeeper和etcd基于会话/租约的自动回收机制与Redis基于过期时间的自动回收机制对比起来讲就已经把这个问题的深度拉满了。这个对比的核心是——前两者把“自动释放”收敛到了中间件自身的会话管理上后者则需要应用层通过定时任务去维护。5.3 三种方案的选型建议没有银弹只有取舍在真实的项目中我总结的选型逻辑是这样的方案性能一致性可用性复杂度典型场景数据库低强一般低低频任务、内部系统Redis高最终一致高中高并发、可容忍极小概率锁失效ZooKeeper中线性一致中中高对一致性和公平性要求高的场景etcd中高线性一致高中高云原生环境配合K8s如果业务场景并发量比较大同时能容忍在极端情况下比如主机宕机、主从切换出现锁的短暂失效那就用Redis。这是最常见的选择。如果业务涉及资金、订单核销这类重数据任何一次锁失效都可能导致严重资损那就不要用Redis直接用etcd。流量大一点没关系性能可以通过其他方式去优化但一致性上不能退让。如果团队已经有成熟的ZooKeeper集群并且应用的并发量并不极端ZooKeeper也是不错的选择毕竟它连客户端断连时的自动释放都能帮你处理好。6. 面试追问环节最容易翻车的四个细节问题最后这部分我把面试中真正容易让人翻车的追问点整理了一遍。这些问题如果答不好哪怕你前面的主干答得再流利整体表现也会被打折扣。我用自己的经历来说这几个问题我基本都踩过或见过别人踩过。问题一Redis的key过期时间应该设置成多少这个问题没有标准答案但你要能说出背后的逻辑。如果业务方法平均耗时是50毫秒锁的过期时间设置个10秒其实足够了不需要为了“保险”设置成5分钟。设置过长的唯一后果是一旦持有锁的节点宕机其他节点要等很久才能恢复。一个好的习惯是统计业务方法在99%情况下的耗时再预留2-3倍的缓冲。如果你在用Redisson这个问题就不需要过度操心因为看门狗会自动续期但你需要确认看门狗是否被正确启用。问题二为什么释放锁的Lua脚本里要先比较value前面已经详细说过这是为了防止误删其他线程的锁。面试时你还可以补充一个细节value值应该用什么生成推荐使用UUID或者threadId UUID的组合保证每个线程获取锁时的value全局唯一。如果value只是一个固定的字符串那校验逻辑形同虚设。问题三如果业务执行时间超过了锁的过期时间会有什么后果这是分布式锁最常见的生产事故之一。如果一个线程执行业务超过锁的过期时间锁被Redis自动删除其他线程会立刻获取到同一把锁两个线程同时执行临界区代码。后果的严重程度取决于业务本身——如果是扣库存就会超卖如果是发放奖励就会重复发放。解决思路有两个一是Redisson的看门狗机制自动续期二是在业务层面做兜底比如更新操作带上条件“仅当当前状态为未处理时更新”即使锁失效了数据一致性依然能被数据库层面的约束保住。这里我想多说一句无论用什么分布式锁业务层面的幂等设计都应该是一个必备的保底手段。因为世界上不存在百分之百可靠的分布式锁只有锁和业务层兜底的双保险才能真正保证数据安全。问题四锁的key被误删了怎么办这里的“误删”原因可能是多方面的网络抖动导致Redis连接被断开、锁过期后被其他线程获取、上一任持有者释放锁时value校验不正确。处理方式没有银弹核心思路是两点第一必须用带value校验的Lua脚本释放锁最大限度避免误删第二在业务上做幂等设计即使锁丢了重复执行业务也不会产生脏数据。这个回答和上面问题三的建议是一脉相承的——大家都说分布式锁怎么实现但真正经历过生产环境的人都知道分布式锁只是保证并发安全的第一道防线业务自身的幂等设计才是最后的底线。7. 面试回答的完整话术结构从开篇到追问的每一步怎么走上面把技术点拆完了最后我再给出一个可以直接拿去参考的回答框架。面试现场的发挥和写文章不一样需要清晰的逻辑线让面试官能跟上你的思路。拿到问题后我会这样组织答案先给总纲。明确说出分布式锁要解决的问题和四个核心要求互斥性、可重入性、防死锁、高性能与高可用。这个总纲能帮你建立整体框架也给了面试官一个信息地图后续他会根据对你框架的兴趣点来选择追问方向。然后做方案演进。从数据库方案讲起说清楚为什么不选它再引出Redis方案按版本演进讲SETNX、SET命令原子化、Lua脚本原子解锁顺带提到Redisson的看门狗与RedLock争议。这一步是整个回答的主体也是面试官注意力最集中的时候。接着讲方案选型。把Redis和ZooKeeper/etcd做一个对比明确不同场景下不同选择的理由。如果你能把选型的思考模式性能优先、一致性优先、公平性优先讲出来面试官通常会开始在心里给你加分。最后是主动暴露思考。聊你实际用过的场景、踩过的坑、对锁粒度设计的理解、对业务幂等兜底的看法。如果面试官没有追问到这里你也可以在回答末尾自然地带一句“这块我之前踩过一个坑还挺典型的可以展开讲吗”。把主动权掌握在自己手里比被动等面试官出招要好得多。整个过程保持边说边想、逻辑清晰、不与面试官争辩。回答的节奏建议控制在5-8分钟给面试官留出追问的空间。如果他一直追问到RedLock的争议点、甚至问到了etcd的底层一致性协议那恭喜你这个话题已经进入了深度交流模式这通常意味着面试官对你的技术深度是认可的态度。