ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式锁面试考点全解析:从实现原理到可靠性边界

分布式锁面试考点全解析:从实现原理到可靠性边界 分布式锁这个话题几乎每一轮后端面试都会出现。我最近把近两年遇到的、还有身边同事被问到的分布式锁面试题归拢了一下大概凑了一百道从最基础的“什么是分布式锁”一路问到RedLock算法到底靠不靠谱、看门狗续期会不会失效、主从切换丢锁怎么办。这篇不打算把一百道题原封不动列出来——那样看着挺全但说实话背完也忘得差不多了。我按考点把题目重新归了类挑出真正会反复问、也最见火候的几个方向展开讲答案是什么、为什么是这么设计、面试官还没明说的潜台词是什么。先说你最关心的分布式锁要掌握到什么程度才能让面试官觉得你是真干过而不是纯背题。1. 分布式锁面试考点地图先知道面试官在考什么1.1 一百道题背后的实际考点分布我把那一百道题重新过了一遍发现很多题其实是同一个考点的不同问法。比如“Redis分布式锁怎么实现”“setnx和set NX EX有什么区别”“为什么不用setnx加锁”这几道本质都在考Redis加锁这个动作的可靠性。真正需要重点准备的其实是下面这五个方向。考点方向题目占比典型问题使用场景与前置判断10%什么业务需要分布式锁单体锁为什么不行Redis分布式锁原理30%set NX EX的细节、过期时间怎么设、解锁为什么用Lua可靠性边界25%主从切换丢锁、锁误删、锁超时、GC停顿其他方案ZK/数据库20%ZK锁的底层原理、和Redis锁怎么选型高阶设计与实战15%可重入锁、续期机制、Redisson源码、线上排查这个分布很能说明问题面试官根本不指望你把分布式锁所有实现都背下来他真正在意的是“你知不知道这东西会出问题以及出问题了怎么兜底”。所以后面我会把重点放在可靠性边界和高阶设计上这两块才是区分“用过”和“会做”的分水岭。1.2 面试官问分布式锁的三个潜台词同一个问题不同水平的候选人给出的答案差别很大。比如“Redis分布式锁怎么实现”初级回答是“用setnx”中级的会补上过期时间和Lua脚本解锁高级的会把主从切换丢锁、RedLock争议、fencing token都说清楚。面试官其实是在用这个问题探测你对分布式系统的理解深度。我自己的理解是分布式锁的考察分成三层。第一层是会不会用API只要写过代码的人基本都知道Redisson的lock和unlock怎么调。第二层是懂不懂原理比如为什么加锁命令必须包含过期时间、为什么解锁要校验value。第三层是有没有踩过坑比如锁到期业务没跑完怎么办、Redis主节点挂了从节点还没同步锁怎么办。绝大多数候选人停在第二层能讲到第三层的面试官基本会给正向评价。所以这篇文章的章节安排本质上是按这三层来组织的先讲场景和基础再深挖Redis原理接着是ZK和数据库对比最后是进阶设计和实战排查。你按这个思路去准备比单纯背题有效得多。2. 基础题分布式锁到底在解决什么问题2.1 单机锁为什么管不住多个服务节点面试高频开场题就是“你为什么要用分布式锁单机的synchronized不行吗”。这道题看着简单其实考的是你对多实例部署的基本认知。建议大家先想清楚一个前提现在的后端服务为了搞高可用和横向扩容几乎不会只部署一个实例而是N个进程一起对外提供服务前面挂负载均衡把请求分发到不同节点上。在这种情况下synchronized和ReentrantLock都锁不住跨进程的共享资源。因为这两把锁是JVM内存层面的只对当前进程内的线程生效。假设库存数量存在数据库里同一时间来了两个请求分别打到服务A和服务B上A和B各自的synchronized只能锁住自己进程里的线程两个进程还是会同时读到库存20然后各自扣减最终库存只扣了1而不是2。这就是典型的并发超卖。我面试时喜欢用“小区门禁”来解释这个事。小区只有一个大门里面每个楼栋还有一个楼栋门楼栋保险门只能拦住本栋的访客拦不住从别栋绕过来的人。服务部署多个节点以后进程级锁就是楼栋门分布式锁才是小区那扇真正拦住所有人的大门。这个类比在解释给非技术同学听的时候很好用。2.2 一个合格的分布式锁要满足的五个条件既然单机锁不行那分布式锁得长成什么样才合格面试里经常从这个问题往下引。我总结成五条每一条背后其实都对应一个具体的坑。第一是互斥性同一时刻只能有一个客户端持有锁这是分布式锁的底线。第二是防死锁客户端持有锁期间崩了或者网络断了锁必须能自动释放否则其他所有客户端都会卡死这对应Redis里的过期时间。第三是可重入同一个客户端如果已经持锁后续再请求锁应该能直接拿到对应可重入锁的计数逻辑。第四是高可用获取锁和释放锁的过程不能因为某个节点挂了就完全不可用。第五是性能要好加锁解锁本身不能太重否则成为系统瓶颈。很多人在面试时只说“互斥、防死锁、可重入”三条其实第四条和第五条才是面试官判断你有没有真实场景经验的分界线。尤其是高可用这一条后面要讲的主从切换丢锁问题就是冲着它来的。2.3 高频使用场景秒杀、定时任务、防重紧接着基础题的就是“你们业务里哪些地方用了分布式锁”。这道题不准备的话现场容易想不起来或者只会说“秒杀扣库存”。实际上分布式锁的典型场景至少能说四类。第一类就是库存扣减这也是最经典的一类秒杀活动的库存总量有限并发请求大量涌入必须用锁保证扣减操作的原子性。第二类是定时任务的多节点执行控制很多系统为了防止定时任务重复执行干脆配了多个节点都跑同一套任务这时候需要用分布式锁选出一个执行者其他节点等着就行。第三类是幂等控制客户重复提交订单、重复点击按钮时用分布式锁保证同一个订单的创建过程只跑一次。第四类是分布式系统中的资源协调比如多个服务要批量处理同一批数据用锁避免重复消费。这几类场景本身不难但面试时最好能补一句“选择分布式锁不代表所有并发都靠锁处理”可以结合分库分表、幂等键、消息队列削峰等手段综合设计。能说出这层取舍答案的层次一下就上去了。3. Redis分布式锁最高频实现也是最大的坑3.1 加锁从setnx到set NX EX的演进为什么必须带过期时间Redis分布式锁是面试里绝对的核心必须先答好“你会怎么实现”。大家最早接触的版本基本是setnx命令也就是SET if Not eXistskey不存在才设置成功多个客户端同时执行只有一个能成功返回1的拿到锁。这个思路是对的但老版本写法有个致命问题这条命令不会自动设置过期时间。假设客户端A用setnx加锁成功结果业务执行到一半程序突然崩了或者机器宕机了。因为根本没设过期时间这个key就永远留在Redis里后面所有客户端都加锁失败整个链路直接瘫痪。这就是所谓的死锁。所以后来演化成把setnx和expire两条命令分开写先setnx再expire设置过期时间。但两条命令不是原子的第1条刚执行完、第2条还没执行时进程崩了死锁问题依然存在。现在大家都用一句话解决SET key value NX EX 30也就是set命令带上NX不存在才设置和EX过期时间这样“判断是否存在写入值设置过期时间”就是原子操作。既然有现成的正确写法面试时就不要再去吹setnx了直接答“用set命令的NX和EX参数”再补一句“不管是setnx还是set核心逻辑都是利用Redis单线程串行执行命令来实现互斥”这就显得你清楚底层原理。3.2 解锁为什么必须用Lua脚本value校验是怎么回事加锁讲完面试官必问解锁。很多人想都不想就说“删掉key就行del lock”这又是一个坑。假设加锁时设的value是“clientA”锁key是“order_lock”业务还没执行完锁因为过期时间到了自动释放了这时候另一个线程B拿到锁也开始干活。偏偏A在B拿锁之后也执行完了A执行del order_lock等于把B的锁给删了。这个场景叫锁误删实线上很常见。解决办法是给每个客户端加锁时生成一个唯一标识作为value比如UUID解锁时先判断当前key的value是不是自己生成的那个UUID是才能删不是就不删。但这里有个细节判断和删除是两个动作如果分开执行判断完刚准备删锁又过期被别人拿走了你还是会把别人的锁删掉。所以这两个动作必须原子地一起执行Redis里实现原子性的办法就是Lua脚本。我贴一个标准解锁脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end把这段脚本通过EVAL指令发给RedisRedis会保证脚本内的所有操作是原子的中间不会有其他命令插入。面试时能把value校验和Lua脚本的原子性讲出来这道题就过关了。3.3 锁过期时间设多少合适业务没执行完怎么办加锁设过期时间看起来简单实际上是个两难问题。时间设短了业务还没跑完锁先过期了其他线程趁虚而入互斥性被破坏时间设长了万一持有锁的节点宕机其他线程要等很久才能拿到锁业务上表现为大面积超时。很多团队默认设30秒但30秒也不是安全的极端情况下大事务、慢SQL、GC停顿都可能超过这个时间。解决这个问题的思路不是精确配一个“完美时长”而是让锁的有效期跟着业务跑核心机制是续期也叫看门狗。Redisson这个库里的做法是加锁成功后起一个后台定时任务每过大概三分之一锁时长就自动给锁续期一次。比如你设锁30秒失效后台每10秒续一次只要业务还在跑锁就一直续业务执行完了显式调用unlock同时把这个后台续期任务停掉。面试官如果追问“你们项目里有没有遇到过锁过期导致并发问题”就可以顺着这个机制答。再说句实在话绝大多数公司不会自己写续期逻辑都是用Redisson现成的lock.tryLock()真正要理解的是它背后的设计思想锁的超时不能拍脑袋定死要有动态调整的兜底。3.4 主从切换丢锁和RedLock这道题最见功力如果说前面几道是基础分那“Redis主从切换的时候锁会丢吗”就是拉开差距的题。Redis为了高可用通常是主从架构加哨兵或者Cluster模式写操作在主节点主节点把数据异步复制给从节点。问题就出在“异步”上主节点刚写入锁key还没来得及同步给从节点主节点就挂了。哨兵把某个从节点提升为新主节点这个新主节点里并没有这把锁其他客户端自然就能重新加锁成功。你想想这个场景有多恐怖客户端A还在执行订单支付流程客户端B已经把同一把锁拿到手了也开始执行支付流程。两个客户端同时觉得自己握着锁互斥性彻底失效。RedLock算法就是为了解决这个问题提出的。它的思路是把锁同时加在N个独立的Redis节点上一般建议N取奇数比如5只要超过N/2个节点加锁成功就认为加锁成功释放锁时对所有节点都释放。但RedLock争议很大分布式系统领域的大佬Martin Kleppmann专门写过文章批评它。他的核心观点之一是即使RedLock在大多数节点上成功了依然无法抵御长期GC暂停和时钟跳跃带来的失效问题。比如客户端A加锁成功然后发生一次长达40秒的Full GC锁过期了客户端B拿到锁开始干活A的GC结束后又接着跑之前的业务逻辑两边就冲突了。你就算把锁加到所有节点上也扛不住进程自身长时间停滞。面试时能把这个背景讲出来说明你真的研究过这个问题而不是只会背结论。4. 除了RedisZooKeeper和数据库也能实现分布式锁4.1 ZooKeeper临时顺序节点原理以及为什么不会惊群Redis锁讲了那么多坑面试官通常会话锋一转“你们有没有考虑过用ZooKeeper来实现分布式锁”这题考察的是你方案选型的能力不是让你把ZK背得多熟。ZK实现锁的基本思路是利用临时顺序节点。具体流程是这样的客户端往指定锁目录下创建一个临时顺序节点比如/lock/lock_0000000001然后查一下这个目录下所有子节点按序号排序。如果自己创建的节点是最小的那个说明拿到了锁如果不是最小的就对自己前一个节点注册一个监听等它被删掉后再去争抢。因为是顺序节点所有客户端都在排队每个客户端只需要盯住自己前一个节点不用所有人都盯着最大那个节点这就避免了“羊群效应”也叫惊群效应。临时节点还有一个关键特性客户端和ZK之间断开连接后节点会自动删除。这意味着持有锁的客户端崩了、网络断了锁也会自动释放不需要依赖过期时间也就没有Redis那种“业务没跑完锁先过期”的问题。面试时可以拿这个点和Redis对比ZK的锁更偏向CP能提供线性一致性Redis锁偏向AP高并发场景下更轻快。4.2 数据库悲观锁和乐观锁面试中的加分项说完Redis和ZK面试官有时还会问一句“只用数据库能不能实现分布式锁”。大部分人听到“数据库”就只想到唯一索引这个方向对但可以再补充悲观锁和乐观锁两个思路。悲观锁就是利用SELECT ... FOR UPDATE。比如要操作订单表里id为1001的订单先执行SELECT * FROM order WHERE id 1001 FOR UPDATE把这一行锁住直到事务提交才释放。数据库锁是天然互斥的多个事务同时执行这条语句时只有一个能成功其余全部阻塞等待。优点是实现简单、严格可靠缺点是并发能力受数据库连接限制而且如果事务里操作时间长会占用数据库连接不释放容易把连接池打满。乐观锁的思路相反不锁行而是靠版本号或者条件更新。比如UPDATE order SET stock stock - 1 WHERE id1001 AND stock 20只有当库存还是20的时候才更新成功否则失败重试。这其实不叫“锁”只是一种防止并发覆盖的手段但它能解决“超卖”这类问题。乐观锁在秒杀这类读多写少、冲突偶尔发生的场景下性能很好但冲突率高的场景会大量重试体验很差。唯一索引方式则适合“防重”场景比如支付回调只希望处理一次给业务主键建唯一索引第二次插入直接报错比分布式锁更轻量。4.3 三个方案怎么选给出你的判断依据面试里常见的问题是“Redis、ZooKeeper、数据库分布式锁选哪个”。这题没有标准答案关键在于你有没有自己的判断逻辑。我的回答思路是分三个维度一致性要求、性能要求、运维成本。一致性要求高、对极端并发下的互斥性有强要求选ZK它能保证分布式环境下严格的线性一致性。性能要求高、基础设施已经有了Redis且能接受极小概率的锁失效选Redis它最简单、最快绝大多数业务场景完全够用。数据库方案一般做兜底或者做防重除非公司里没有Redis也没有ZK否则不建议用数据库当主力分布式锁性能和可用性都容易出问题。面试时如果能加一句“选型要先问你自己的业务底线是什么能接受什么程度的风险”而不是直接报一个方案名字这个答案就比较成熟了。我见过几个候选人都是不说套话、直接举自己团队例子比如“我们之前用Redis后来因为主从切换丢过一次锁讨论过要不要换ZK最后发现业务可以容忍偶发重复就继续用Redis了”这种回答明显更有说服力。5. 进阶设计可重入锁、续期机制和时钟跳跃5.1 Redis怎么实现一个可重入锁底层数据结构是什么面试题问到“分布式锁怎么处理重入”很多人的第一反应是用ThreadLocal记录当前线程是否持锁。这个思路能用但Redisson并不是这么干的。Redis实现可重入锁的经典做法是用Hash结构key还是锁的名字hash里的field存客户端/线程的唯一标识value存一个计数器。每次加锁时判断hash里field是否存在。如果不存在说明是第一次加锁hset写入field线程标识value1同时设置过期时间如果field已经存在且等于当前线程标识说明是同一个线程重入就把value加1。释放锁时先把计数器减1减到0才真正删除整个key。这样同一个线程多次lock就对应多次计数只有计数归零锁才真正释放和JVM里ReentrantLock的思路一模一样只不过存储状态的地方从内存换成了Redis。这里有个容易被追问的细节可重入锁的执行过程由好几个Redis命令组成比如hset、hexists、hincrby、expire为什么不会出现并发问题答案和单线程模型有关Redis所有命令都是单线程串行执行的但一串命令之间如果被其他客户端命令插进来还是会出问题。所以Redisson这些操作全部封装成了一段Lua脚本脚本整体交给Redis执行保证不可分割。5.2 看门狗续期的底层逻辑以及它解决不了的问题前面提到看门狗续期这里把细节讲透。Redisson默认的锁过期时间其实是30秒也就是leaseTime。加锁成功后会启动一个定时任务每隔锁时间的三分之一也就是10秒检查一次锁是否还被当前线程持有如果持有就重新设置过期时间到30秒。这个定时任务在unlock之后会被取消避免锁一直续期不释放。这个机制确实解决了“业务执行时间超过锁时长”的问题但它有一个前提进程本身要正常运行。如果客户端发生了长时间GC停顿或者宿主机被暂停看门狗的续期线程一样被冻住锁过期后依然会被别人抢到。等业务线程从GC中恢复它自认为仍然持锁其实锁已经不属于它了。这就是前面说的“GC pause导致锁失效”属于分布式锁本身无法彻底解决的问题只能通过fencing token之类的机制从业务层面兜底。面试时如果把看门狗讲完再主动提一句“虽然Redisson会自动续期但极端停顿下锁还是会失效所以我们线上会接受很小的重复概率通过幂等处理来兜底”面试官基本不会再往深处追问了。能接受不一致而且有兜底这才是大厂分布式系统里真实的处理方式。5.3 时钟跳跃问题一个容易被忽略的边界场景另一道容易被忽略的进阶题是“如果Redis服务器或者客户端服务器时钟发生了跳跃分布式锁会出现什么问题”。锁的安全性其实依赖于时间判断节点A加锁成功锁的过期时间戳是t1如果这时候Redis服务器的时钟突然往前调了一大段比如t1直接变成过去时间锁就可能立刻过期其他人马上能拿到锁。反过来如果时钟往后调锁的有效期又会莫名变长该释放的锁迟迟不释放。解决思路有两个层面。第一是运维层面使用NTP服务保证服务器时钟尽量准确并配置合适的时间校准策略避免大幅跳跃。第二是设计层面不要单纯依赖时间判断比如引入自增序号、写操作带上递增token让业务侧能识别出“这个并发请求是不是来自已经过期的锁持有者”。时钟跳跃在实际面试中出现频率不高但能答出来会显得边界意识很强因为做分布式系统最怕的就是默认“大家的时钟都是一样的”。6. 实战踩坑记录与面试连环追问怎么接6.1 我线上踩过的四个分布式锁的坑前面原理讲得多这里分享几个我真实踩过的坑面试的时候直接拿来当案例讲效果比背定义好得多。第一个坑是锁超时导致同样的逻辑执行了两遍。我们有一次做订单状态回写A服务加锁处理订单处理逻辑里有一次很慢的外部HTTP调用耗时超过了锁的过期时间锁被自动释放B服务也拿到锁开始处理同一个订单。两边重复处理下游收到两条内容一样的回调。这个问题的修复方式很直接外部调用尽量移出锁内执行或者把锁时间调大配合看门狗续期。第二个坑是锁误删。早期我们加锁只用固定的字符串做value比如LOCK也写了del结果两个服务之间会出现互相删锁的事。发现之后把所有加锁请求改成了UUID作为value解锁统一走Lua脚本。第三个坑是单点故障。我们把锁加到单个Redis主节点上有一次主节点宕机由于没有配置哨兵自动切换整个业务锁服务对下游完全不可用所有需要抢锁的接口全部报错。后来加了高可用方案也在关键路径上做了降级策略。第四个坑是锁粒度太大。有一段时间我们想省事把一类订单的整个处理过程都放在同一把锁里结果所有订单都串行处理接口RT从几十毫秒涨到几秒。后来改成按订单ID维度加锁锁粒度细化到单个业务对象。这几个坑在面试里就是活生生的例子比讲一百遍原理都有说服力。6.2 面试官连环追问的六个套路分享几个面试现场高频出现的连环追问每个都给一个可以直接使用的回答方向。“Redis是集群模式的话锁加在主节点还是从节点”答Redis Cluster场景下锁会落在某个slot对应的主节点上主节点同步给从节点如果主节点挂掉且没有及时同步理论上锁会失效这也是RedLock要解决的问题。“锁的过期时间为什么要设成30秒而不是60秒”答没有统一标准。30秒是Redisson的默认值配合看门狗续期基本够用。如果业务里确实有超过30秒的长事务要么调大初始值要么保证续期线程不被阻塞。“业务代码和unlock之间抛异常了怎么办”答把解锁逻辑放在finally里或者用Redisson自带的lock()方法在注解模式下处理。绝对不能只依赖自动过期异常路径必须显式释放锁。“分布式锁和分布式事务是什么关系”答两者不是一回事。分布式锁解决的是并发互斥问题分布式事务解决的是跨节点数据一致性问题。锁只能保证同一时刻一个人操作不能保证操作结果在多个库之间原子提交。“你遇到分布式锁失效会怎么排查”答先从现象判断是锁没抢到还是抢到了锁但互斥失效然后看Redis里的key状态、value是不是自己的、TTL还剩多久再看业务日志里有么有进入临界区的记录最后结合链路追踪确认是否有锁超时、主从切换等异常事件。“有没有不用锁的替代方案”答有。比如用乐观锁版本号、用队列串行化、用幂等表去重、用唯一约束兜底。分布式锁不是唯一答案很多场景下用更轻量的方案反而更好。这几个追问一旦接住后面基本就是闲聊了。6.3 一套通用排查思路分布式锁问题三板斧整理一个自己常用的排查套路面试被问到实战题时可以边说边展示思路。第一板斧是确认锁本身的状态。先查Redis里锁key是否存在、value是不是当前业务实例的标识、TTL还剩多少。如果key没了但业务日志显示已经加锁了那大概率是锁过期被释放了。第二板斧是确认业务有没有重复执行。去下游或者数据库里看同一笔业务产生了两次明显记录。如果重复了把两次操作的时间点和持锁方日志拉出来对比很容易看到是不是锁超时窗口期内另一个节点进入了临界区。第三板斧是确认基础设施有没有异常。查一下当时Redis有没有主从切换、节点有没有重启、网络有没有抖动。这三板斧走完绝大多数分布式锁问题都能定位到原因。面试时把这三步说出来说明你是真的见过线上事故的人而不是只会背理论。最后分享一点个人心得我从一开始用setnx实现分布式锁到后来慢慢研究Redisson源码、踩过主从切换丢锁的坑、又回头去读RedLock的争论文章整个过程最大的体会是分布式锁不是一个“调API”的问题而是一个“设计可靠性边界”的问题。面试官不会因为你背下来某个框架的用法就给你高分真正值钱的是你知道这套机制在什么情况下会失效、失效后会造成什么后果、怎么从业务层面兜底。如果你正在准备面试建议把文章里的场景自己捋一遍试着不用文档也能说清楚set命令为什么带NX和EX解锁Lua脚本为什么能防止误删看门狗续期解决什么问题、解决不了什么问题ZK临时顺序节点怎么避免惊群主从切换丢锁时你的业务有没有幂等兜底。把这些串起来再往里填一两个自己项目的真实案例这道分布式锁的题就稳了。
返回列表