最近很多准备面试的朋友都在问Redis分布式锁,尤其是那几道高频面试题,问来问去总是绕不开“怎么实现”“怎么坑了”“怎么优化”。我做了多年分布式系统,也面试过不少候选人,这里就把Redis分布式锁最常见的8个面试题整理成一篇答疑文章,每个题目都给出原理拆解和实操建议,重点讲清楚面试官到底想听到什么,以及你在实际开发里会碰到的那些细节。不管你是刚接触分布式锁,还是已经用过Redisson,这篇文章都能帮你把答案组织得更完整。
1. 分布式锁的基础概念
1.1 分布式锁的应用场景
分布式锁说白了就是解决“多个进程/多台机器同时操作同一个资源”时的冲突问题。单机环境下我们能用synchronized或ReentrantLock,但那只是进程内锁,换成微服务部署后在多台机器上就失效了。分布式锁要让所有节点都守同一个规矩:同一时刻只有一个实例能拿到锁,其他实例必须等待或者失败。
常见的应用场景包括:库存扣减、优惠券发放、定时任务多节点防重复执行、缓存重建的保护、支付回调幂等处理等。比如两个服务同时处理同一个订单的支付回调,如果不加锁,就可能重复入账。分布式锁在这里承担的就是“全局互斥”的角色。
1.2 Redis实现锁的核心命令
Redis实现分布式锁的核心命令是SET key value NX EX seconds。这条命令同时做到两件事:NX保证只有当key不存在时才能设置成功,也就是“抢锁”的原子性;EX给key设置过期时间,防止拿到锁的线程挂了导致死锁。这是最基础也最关键的原子操作。
SET lock_key unique_value NX EX 30000锁的释放用Lua脚本保证原子性:先比对key对应的value是否是自己当初设置的那个,如果是再删除,避免误删别人的锁。Redis单线程模型天然支持原子执行Lua,所以这条路径是安全的。这里unique_value一般是UUID或者业务标识,用来标记锁的持有者。
2. 8个常见面试题与深度解读
2.1 面试题1:如何基于Redis实现一个分布式锁?
这道题是基础中的基础,你至少要把加锁、解锁、超时三步说清楚。加锁用SET lock_key unique_value NX EX expireTime,锁值必须唯一,通常用UUID。为什么必须唯一?因为解锁时要校验持有者,否则可能出现A线程锁超时被B线程接管,B还没执行完,A带着旧锁去删,结果把B的锁删了。
解锁必须用Lua脚本,不能先查询再删除,因为“查询+删除”不是原子的,中间可能被别的线程插进来。标准解锁脚本如下:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end给key设过期时间是为了解决持有锁的节点宕机后锁永远不释放的问题。过期时间长短要看业务时间,比如一个同步操作需要5秒,你可以设置10秒甚至更长。太短可能业务没做完锁就释放了,太长又会增加锁被某个故障节点一直占用的时间。
这道题还有一个升级问法:如果只靠单节点Redis,主从切换时锁会丢。比如客户端A在主节点上拿到锁,主节点还没同步到从节点就挂了,从节点升级为主节点,B客户端就能在旧主节点上再拿一次锁,锁就失效了。这是单节点Redis方案最典型的缺陷,面试时要主动提出来,并且说明可以用RedLock等方案,但那也不是绝对完美,后面会细讲。
2.2 面试题2:为什么说Redis分布式锁不够安全?
这题考察你对分布式系统知识的深度。很多候选人能答出加锁解扣,但深入问一下就会卡壳。核心不安全点一共有三个。
第一,上面提到的锁丢失问题:主从节点异步复制导致锁在故障转移时丢失,严格来说这不满足分布式锁的可靠性要求。第二,锁过期与业务执行时长冲突:如果业务耗时超过了锁的过期时间,锁自动释放后,其他线程就能抢到锁,两个线程同时执行临界区,互斥被打破。第三,Redis的时钟跳跃或GC停顿也可能让锁过期计算出现偏差,尤其在持久化或虚拟机环境下。
更严谨地说,分布式锁要保证“安全性和活性”,安全性要严格保证互斥,Redis锁在极端情况下做不到;活性要求锁最终能释放,这个相对容易。所以很多大型业务并不把Redis分布式锁当作“绝对正确”的锁,而是当作“高可用、高性能、容许极小概率冲突”的协调工具,配合数据库唯一索引或者消息幂等来做最终的兜底。
这里有一个实用的补充:如果你对安全性要求极高,可以考虑使用Zookeeper锁或数据库行锁,但代价是吞吐量和复杂度明显上升。不存在一个方案在所有场景都完美,选型必须看业务容忍度。
2.3 面试题3:如何解决锁超时导致的问题?
锁超时是分布式锁在实际项目中踩得最多的坑。比如一个线程执行一个耗时任务,锁过期时间设为5秒,但实际上任务跑了10秒,5秒后锁释放,另一个线程进入临界区,两个线程并发执行,整个分布式锁形同虚设。
解决思路无非三点。第一,过期时间尽量设置得比预估执行时间大得多,但也不能无限制,否则故障节点问题更大。第二,引入“锁续租”机制,核心是给锁设置合理的较短过期比如10秒,通过后台守护线程每过三分之一或二分之一时间就检查一次:如果业务还在执行,就重新设置过期时间,直到锁释放。这个看门狗机制Redisson已经内置实现,面试时提到会加分。
实现续租时要注意:如果使用EXPIRE frisk直接延长锁的过期时间,必须保证只有锁的持有者才能续租,否则旧持有者可能把别人的锁续上。常见的做法是使用Lua脚本校验value后再执行PEXPIRE。也可以用EXPIRE lock_key expire_time配合锁值校验,但一定要走脚本保证原子性。
第三,业务侧做“防重入校验”:即使锁过期导致并发进入,在写数据库或者修改状态时用乐观锁、唯一约束等方式兜底,把分布式锁当成一级防护而不是唯一防线。这也是面试官期待的答案之一——分布式锁从来不是银弹,下游幂等才是最终安全网。
2.4 面试题4:如何避免锁误删?
锁误删指当前线程释放锁时,删除了别的线程持有的锁。产生原因就是锁值不唯一、释放锁时没有做持有者校验。比如线程A加锁成功,value是UUID-A,随后A阻塞或超时,锁自动过期;线程B加锁成功,value是UUID-B;A恢复后执行解锁脚本,如果不比对value直接DEL lock_key,就会把B的锁删掉,导致B失去互斥保护。
解决办法很简单:解锁时必须比对锁值。Redis官方推荐用Lua脚本原子地“比对+删除”,只有GET lock_key的结果等于当前线程自己的value时,才执行DEL。这样A的解锁操作就不会误删B的锁。关键是要保证value的唯一性,得用随机字符串或UUID。
这里还牵出一个容易忽略的问题:锁值不能放在ThreadLocal里随便覆盖,多线程环境要注意隔离。明确每个线程加锁时生成一个独立的value,并且这个value要和执行上下文绑定,解锁时从上下文取出来再校验。我在实践中用过一个简单办法——用“业务标识+线程ID+UUID”拼成一个value,排查问题时非常直观。
2.5 面试题5:如何实现可重入的分布式锁?
可重入指的是同一个线程在持锁的情况下可以再次加锁而不会把自己阻塞。单机锁靠线程维度计数就能实现,分布式锁要考虑的是Redis怎么记录“同一个持有者已经加了几次锁”。
常用的实现思路是给锁值里面拼接一个加锁次数。比如value本身就是JSON:{"clientId":"UUID","count":1}。每次加锁时先校验当前锁是否属于当前持有者:如果是,把count加1;如果不是,就按普通加锁处理。释放锁时,每次减1,只有当count减到0时才真正删除key。这个“判断归属+更新计数”的过程必须用Lua脚本原子化,否则并发下计数可能错乱。
Redisson内置的RLock就是可重入锁,它的getLock支持同线程重入,内部通过threadId维护锁状态。在面试中,你能说出“重入计数存哪里,用什么保证原子性”这两点,就足够展现你对细节的理解了。需要额外提醒的是,可重入虽然好用,但别滥用,因为每次重入都要访问Redis,而且锁的语义会变得复杂。很多场景下,优化业务逻辑让方法不要再进同一把锁,才是更干净的方案。
2.6 面试题6:Redisson的看门狗机制是如何工作的?
Redisson是目前最流行的Redis分布式锁客户端库,它把分布式锁的很多细节都封装好了,面试问它是想考察你对成熟方案的掌握程度。
看门狗机制的思路很简单:拿到锁之后,会启动一个后台定时任务(默认叫Watchdog),周期性检查锁是否还存活。默认锁超时时间是30秒,看门狗每10秒检查一次。如果业务线程还活着,就自动把锁的过期时间重置为30秒;如果业务已经完成,锁被释放,看门狗就撤销。这套机制有效解决了“业务没跑完锁却过期”的问题。
值得深究的细节有两点。第一,看门狗是“续期锁”,不是“续期线程”。它只是在锁还由当前线程持有的时候,不断把key的过期时间重置。第二,看门狗本身是一个Daemon线程,项目里有大量锁时要注意线程资源和Redis连接开销。另外,只有当使用默认的锁超时时间时看门狗才生效;如果你在加锁时手动指定了leaseTime,Redisson不会再启动看门狗续期,因为锁到期就要释放,这时你要自行承担业务超时风险。这是一个非常容易踩的细节,面试时主动说出来非常加分。
代码层面,使用Redisson的RLock时,客户端会向Redis发送一段Lua脚本执行加锁,之后每一个续期请求也是Lua脚本。所以面试官如果真的追问到底层,你可以从Lua脚本内容展开。
2.7 面试题7:分布式锁集群模式下如何保证一致性?
Redis集群模式下,常见的部署形态有主从、哨兵和Cluster。主从架构下,客户端写主节点,主节点异步复制给从节点。如果主节点在加锁成功但复制未完成时宕机,从节点被提升为主节点,另一个客户端就能成功加锁,原锁失效。这就是分布式锁的一致性问题。
业界应对思路之一是RedLock算法:向多个独立的Redis节点发起加锁,只有当超过半数节点加锁成功,并且总耗时小于锁过期时间,才认为加锁成功;释放锁时向所有节点发送释放命令。算法本身能降低锁丢失的概率,但仍有争议,因为分布式系统里的节点宕机、线程停顿、网络分区都会破坏它的数学模型。作为面试者,你不需要完全否定RedLock,但要说清楚它“降低了风险,但不能绝对保证”。
更务实的做法是:把Redis分布式锁的使用场景控制在“允许极小概率错误冲突,但业务层用幂等兜底”的范围。比如库存扣减,即使锁偶发失效,数据库层的校验和乐观锁还能挡住超额。面试时能讲出这层权衡,比死背一个标准答案强得多。
2.8 面试题8:分布式锁有哪些替代方案,如何选型?
这个题目考的是知识广度和架构决策能力。常见的分布式锁方案有以下几类。
- Redis:高性能,易实现,适合对响应时间要求高、允许极小概率冲突的业务。
- Zookeeper:基于临时顺序节点和Watch机制,锁语义更强,没有锁过期问题,主节点宕机后会话失效自动释放锁,但ZooKeeper本身吞吐低于Redis,且网络开销更大。
- etcd:类似Zookeeper,租约和续期机制完善,云原生场景常用,但引入成本不低。
- 数据库:比如用一张表配上唯一约束,通过
SELECT FOR UPDATE或者插入一条唯一记录抢锁。实现最简单,但性能差,一般只在业务量不大的内部系统中使用。
选型不是选最强最全的,而是看业务的“正确性要求”和“性能要求”。比如支付和财务相关操作,我会优先考虑数据库约束或带事务的锁;如果只是防缓存击穿、防定时任务重复执行,Redis锁配合幂等已经足够。公司在选型时还需要考虑团队技术栈、运维成本和依赖组件的稳定性,这些在面试中可以主动聊出来。
3. 实战过程中的避坑清单
3.1 锁粒度与性能取舍
很多人在实际项目中把一把锁做得过大,比如锁整个用户维度,导致并发量上不去。锁粒度太粗的后果是明明不冲突的操作也被强制串行。合理的做法是尽量缩小锁的粒度,比如锁“用户ID+订单ID”,甚至锁“单据号”,让无冲突请求并行执行。
不过粒度也不能无限细,因为锁本身有Redis网络开销。纯本地操作在5毫秒内完成,加一次锁平均消耗0.5毫秒,这个比例可以接受;但如果业务里频繁加锁解锁,0.5毫秒的累积成本会让单机吞吐下降明显。所以实际操作中,我会先测量业务耗时的分布,再决定锁粒度。热点资源比如秒杀库存,还可以设计成分段锁:把库存分成多个bucket,每个bucket一个锁,请求挂了hash落到不同bucket,减少争抢。
3.2 锁的续期与守护线程
续期机制的实现质量直接决定分布式锁的稳定性。简单场景,可以在加锁后起一个定时任务固定每3秒续期一次,直到业务完成调用unlock。复杂的框架比如Redisson,已经为你做完了这些事。如果你不想引入Redisson,自己实现续期时一定要加一个“持有者标记”的校验,防止旧持有者给新持有者的锁续期。
另一个容易被忽略的坑是线程池场景下的锁续期:定时任务使用的是守护线程,如果应用进程崩了或者线程池关闭,续期也就停了,锁会在剩余时间段内过期。这是好事,相当于一种兜底自愈。但如果业务线程还在跑而JVM被强制关闭,业务会留下半截状态。补偿的办法是业务侧做事务和幂等,别指望分布式锁替你全包。
3.3 锁的公平性与等待队列
Redis分布式锁天然不是公平锁,谁抢到就是谁的,不存在先来后到。如果业务上有严格顺序要求,就得另想办法。Redisson也支持公平锁RLock的公平模式,但实现复杂、性能影响大,在绝大多数处理幂等或并发控制场景下没必要。
如果你需要“等待锁释放后再尝试”的阻塞语义,通常会用一个简单的循环:加锁失败后休眠一小段时间(比如50毫秒)再重试,同时设置最大尝试次数或者最大等待时间,避免无限自旋。需要注意的是,等待时间不能太长,否则请求线程会占满你的线程池。一个均衡的做法是加锁失败后立刻返回,让上层决定是重试、熔断还是降级。
4. 如何组织高分面试回答
4.1 回答里的加分细节
上面的8个问题,面试官真正想考察的其实是这几件事:你有没有真正在项目里用过错、会不会考虑边界情况、有没有意识去设计一个高可用的锁方案。所以回答时不要只背概念,要主动带上自己项目里的参数、异常日志、压测数据。比如你说“我遇到过一次锁误删,原因是value值用了固定字符串”,面试官就会觉得你有实战味道。
回答顺序也很重要。先说答案主逻辑,再讲你踩过的坑,最后说你会怎么优化。不要一上来把几十行代码背一遍,面试官关注的是思路。实操里,我习惯用“场景+方案+代价”的结构:这个场景为什么需要锁,用什么方案实现,带来的性能或一致性代价是什么,如何处理。
4.2 容易被追问的深水区
面试官最喜欢接着追问的几个深水区包括:锁过期时间怎么设置才算合理?Redisson看门狗遇到GC长停顿会发生什么?主从切换一瞬间到底丢没丢锁?Redis的AE事件循环卡住了会不会影响锁续期?你要能承认“这里会有极小概率问题,所以业务侧必须兜底”,而不是硬说自己的方案绝对安全。
如果时间允许,你还可以补充一点:在生产环境,要监控分布式锁的加锁延迟、重试次数、锁持有时间分布。出现异常时,通过日志里的value关联业务请求,快速定位是哪台机器持锁时间过长。这些细节用来回答“你如何保障分布式锁的高可用”之类开放题,非常有说服力。
我个人比较看重“锁作为第一道门,幂等作为第二道门”的设计思路。分布式锁天然有瓶颈和不确定性,但把它和业务校验、数据库防重结合以后,绝大多数场景都能稳定运行。我记得有一次压测,Redis锁偶尔因为主从切换出现双写冲突,但数据库唯一索引挡住了最终那几条脏数据,这个案例让我彻底明白了兜底设计的重要性。如果你想在简历上写“掌握Redis分布式锁”,至少要把本文中那8个问题的底层原理都过一遍,能用自己的话讲清楚,面试这块基本就稳了。