尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Redis分布式锁面试全解析:从SETNX到看门狗,一文搞定八股文

Redis分布式锁面试全解析:从SETNX到看门狗,一文搞定八股文 前两天有个读者私信我说一面的时候业务面试官突然丢了句你们项目里分布式锁怎么用的讲讲原理他当场脑子一懵憋了句SETNX加锁、DEL释放锁然后就没有然后了。面试官追问了三个问题直接把他送走锁过期了业务没执行完怎么办主从切换锁丢了怎么办你释放锁的时候有没有可能把别人的锁删掉这道题我太熟了分布式锁几乎是Java后端面试里必出的保留项目。表面上考的是Redis命令实际上考的是并发设计、异常兜底、故障思维和工程权衡。标题里说的全套八股文说白了就是把这几个维度的问题全部串起来而不是机械背题。今天我就按面试准备的口径把这套东西完整梳理一遍保证你能从知道SETNX进化到能和面试官正面刚。1. 这道面试题背后到底在考什么1.1 单机锁为什么会失效先把场景铺开。你有一个库存扣减接口单机部署的时候用synchronized或者ReentrantLock加锁同一个JVM里的线程互相排他数据不出问题。但一旦上了多实例部署A实例和B实例同时收到扣减请求两个JVM各有一把锁互不相干这时候本地锁就形同虚设。这就是分布式锁要解决的核心问题多个进程不限于Java任何语言都一样之间围绕同一个共享资源做互斥访问。面试官问你分布式锁第一层是想确认你有没有真正遇到过分布式环境下的并发问题还是只在demo里见过Transactional加synchronized。再说个细节有些候选人会把数据库唯一索引和分布式锁混为一谈。唯一索引确实能挡住重复插入但它解决的是幂等问题不是互斥问题。分布式锁要保证的是同一时刻只有一个执行体能进入临界区。这两个概念边界必须清晰否则后面一追问就露馅。1.2 面试官真正想听的能力模型分布式锁这道题面试官实际上在考察三个层次第一层是工具使用能力你知不知道Redis里有SETNX、SETEX这些命令知不知道怎么组合成一把锁。第二层是异常处理能力锁超时了怎么办进程挂了锁会不会自动释放释放锁的时候怎么避免误删别人的锁。第三层是架构权衡能力为什么选Redis而不是数据库或者ZooKeeperRedLock到底靠谱不靠谱锁过期时间和业务执行时间怎么匹配。大部分候选人能过第一层死在第二层第三层基本空白。所以你准备八股文的时候别只背命令要把每一层的问题都提前演练一遍。后文我按这个能力模型展开你对着自查。2. 三种主流实现方案先把全局图装进脑子2.1 数据库锁最朴素但最坑的方案用数据库实现分布式锁最粗暴的方式就是建一张锁表比如lock表里有一条唯一键记录method_name谁想获取锁就插入一条记录插入成功代表拿到锁用完删除记录代表释放锁。这个方案的优点是实现极其简单依赖现成的数据库不需要引入新组件。但坑点非常密集。第一性能上限低每次加解锁都是一次数据库写操作高并发下数据库会成为瓶颈。第二没有自动过期机制如果拿到锁的线程宕机了记录永远留在表里其他线程永远拿不到锁必须额外起一个定时任务做清理。第三数据库主从切换的时候如果从库还没同步到加锁记录另一个请求就能成功插入锁就失效了。如果非要用数据库方案可以配合SELECT ... FOR UPDATE做悲观锁利用数据库的行锁机制实现互斥。这个方案的好处是事务提交自动释放锁不会出现死锁。但性能问题更严重而且长时间持锁会拖垮数据库连接池。我只在内部管理后台这种低并发场景见过这类用法线上高并发核心链路基本没人这么干。2.2 ZooKeeper/etcd 临时节点锁可靠但成本高ZooKeeper实现分布式锁的原理是在指定目录下创建临时顺序节点每个客户端创建后获取当前目录下的所有子节点如果自己是最小序号的那个就拿到锁否则监听前一个节点的删除事件等前一个节点释放后自己再尝试获取锁。这套机制的好处是临时节点会随会话断开而自动删除天然解决了持有者宕机导致锁无法释放的问题节点的顺序性保证了公平锁ZooKeeper集群的ZAB协议保证了数据一致性不会出现主从切换丢锁的情况。所以从安全性和可靠性角度ZooKeeper锁是最稳的。但它的代价是引入一套独立的协调服务运维成本高而且加解锁过程中包含网络RTT和节点监听性能比Redis差一个量级。etcd的方案类似基于租约lease和revision实现可靠性同样好但同样需要额外组件。面试里提到底层实现即可除非你的项目里真的用了这些组件否则别展开太多容易引到不熟悉的领域。2.3 Redis 分布式锁性能和工程实践的首选Redis实现分布式锁的核心优势就两个字快。纯内存操作单线程执行命令天然串行化加锁和释放锁都是微秒级。而且Redis作为缓存中间件绝大数团队已经有现成集群不需要额外引入组件。Redis锁的基本思路很简单利用SET key value NX EX seconds命令的原子性key存在则设置失败相当于互斥配合过期时间避免宕机死锁。这也是面试里必须张口就来的答案。但Redis锁并不完美它的最大隐患是Redis主从架构默认是异步复制主节点拿到锁后还没同步给从节点就宕机了从节点顶上后锁就丢了。这个问题我放到第4节细讲它是面试官最爱的进阶考点也是区分候选人是背题还是真理解的关键分水岭。下面用一个表格把这三种方案的核心差异拉出来面试的时候如果被问你为什么要选Redis直接按这个逻辑答方案实现难度性能自动释放可靠性与一致性适用场景数据库低低需额外任务依赖数据库事务主从切换有风险低并发后台任务ZooKeeper/etcd高中天然支持高一致性协议保证对安全性要求极高的场景Redis低高支持过期时间主从异步复制有窗口高并发、核心链路3. Redis 分布式锁的核心细节一句都不能说错3.1 加锁SET NX EX 的完整语义先说结论现在加锁的正确姿势是SET lock_key unique_value NX EX 30这条命令里NX表示只有key不存在时才设置成功EX 30表示过期时间30秒。SET命令本身是原子的所以不用担心中间插入其他命令。这里有两个细节必须强调。第一value必须设置为一个唯一标识。很多人写demo的时候图省事SET lock_key 1 NX EX 30所有线程都用同一个固定值。这会带来一个严重的问题释放锁的时候A线程直接DEL lock_key如果此时锁已经过期被B线程获取A就把B的锁删掉了。所以value必须是当前线程/请求的唯一ID比如UUID或者requestId释放锁之前先校验value是否正确。第二过期时间必须要设置而且要用EX或者PX不要拆成两条命令。早年有些人写SETNX lock_key value之后单独再执行EXPIRE lock_key 30这两步之间如果进程崩溃锁就永远不释放形成死锁。SET命令合并了这两步的原子性这是面试里很爱挖的坑。另外提醒一句网上有些老文章还在推荐SETNX EXPIRE的组合你要能明确说出这个方案的缺陷和替代方案这本身就是加分项。3.2 释放锁Lua 脚本保证原子性释放锁的正确姿势是先比对value是不是自己的唯一标识是才删。但这里有个隐患比对和删除是两步操作如果中间锁过期了就会出现误删。比如A校验通过突然GC停顿锁过期后被B获取A恢复后执行DEL把B的锁删了。解决办法是用Lua脚本把两步封装成一个原子操作if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endRedis执行Lua脚本是原子的整个脚本执行期间不会插入其他命令所以判断删除要么都执行要么都不执行。这段脚本在Redisson的源码里就有现成实现面试能默写出来基本等于把这个知识点吃透了。实际操作中Java侧如果用Spring Data Redis可以这样调用String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); Long result redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);这里有个我自己踩过的坑DefaultRedisScript如果不显式指定resultType在部分版本的Spring Data Redis里会报类型转换异常而且这个异常通常直到运行时才暴露。所以写完之后一定要在测试环境带上真实Redis跑一遍释放逻辑别只做单元测试mock。3.3 可重入与看门狗续期面试官如果追问你们的锁支持可重入吗你先别急着说加个计数器就行。可重入的语义是同一个线程可以多次获取同一把锁并且要释放相同次数才能真正释放。自己在Redis里实现可重入常用方案是在value里保存线程标识和重入次数的组合信息可以用Redis的Hash结构field存线程标识value存次数。每次加锁判断field是否存在存在则HINCRBY加一释放锁时递减减到0才删除key。这种实现能跑但边界情况很多比如锁超时续期和重入计数的联动处理起来相当繁琐。实际工程中我建议直接用Redisson。Redisson的RLock天然支持可重入而且它内置了看门狗机制RLock lock redissonClient.getLock(lockKey); boolean acquired false; try { acquired lock.tryLock(5, TimeUnit.SECONDS); if (acquired) { // 业务逻辑 } } finally { if (acquired) { lock.unlock(); } }看门狗的原理是如果获取锁时没有指定leaseTimeRedisson会默认给锁设置30秒过期时间然后启动一个定时任务每10秒检查一次锁是否还持有如果持有就把过期时间重置为30秒。这就解决了锁过期但业务还没执行完的问题。这个机制的代价是如果持有锁的实例彻底宕机看门狗也会停掉锁会在30秒后自动释放不会死锁。但有个极端场景Full GC导致看门狗线程暂停了几十秒锁可能已经过期被其他线程获取原线程恢复后继续执行临界区就并发进去了。这个问题没有完美解只能尽量缩短GC停顿或者接受这种极小概率的并发窗口我在第5节再展开。3.4 RedLock 与它的争议Redis官方提出过一个RedLock算法思路是部署5个独立的Redis节点注意是独立不是主从客户端依次向所有节点加锁如果能在有效时间内成功加锁超过半数节点N/2 1就算获取锁成功释放时向所有节点发送释放命令。RedLock想解决的是主从切换丢锁的问题即使一个节点挂了其他节点还持有锁就能保持互斥。但Redis作者antirez和分布式系统专家Martin Kleppmann曾经专门争论过这个算法核心焦点是RedLock依赖时钟假设如果某个节点发生时钟跳跃锁的有效时间会被错误延长互斥性就会失效。面试里被问到RedLock我的建议是观点先摆出来RedLock在理论上有争议实际工程中用得也少因为它引入5个节点的成本和复杂度换来的是一个连设计者自己都承认不是绝对安全的方案。真正务实的做法是如果业务对锁的安全性要求极高直接换ZooKeeper或etcd如果对性能敏感且能接受理论上的极端窗口就用单节点Redis加主从切换兜底。把技术选型是取舍不是堆料这个思想表达出来面试官通常会很认可。4. 面试官最爱的连环追问怎么接住4.1 锁过期了业务还没执行完怎么办这个问题几乎必问因为它是分布式锁最容易翻车的场景。最简单的答案把过期时间设置得足够长比如业务最长执行时间的3倍。但这个方法治标不治本你无法精确预估所有情况而且过期时间太长一旦持有者宕机锁要很久才能释放影响可用性。进阶答案是锁续期。用Redisson的话就是看门狗机制每隔一段时间自动延长锁的过期时间直到业务执行完主动释放。这里你要能说出看门狗的默认参数默认锁超时30秒每10秒续期一次。而且Redisson的实现里续期操作同样是基于Lua脚本先校验当前锁的value是不是本客户端是才EXPIRE。我自己踩过坑的地方是用了tryLock(waitTime, leaseTime, TimeUnit)并且显式传了leaseTime此时看门狗会失效锁到期后自动释放不会续期。很多文章没说这个细节导致业务没跑完锁就没了。如果你需要看门狗续期就一定不要传leaseTime。4.2 主从切换导致锁丢失怎么处理主从架构下客户端A在主节点加锁成功主节点异步复制给从节点。如果主节点在复制完成前宕机从节点被提升为新主节点但新主节点上没有锁记录。此时客户端B也去加锁成功了。A和B同时持有锁互斥被破坏。这个问题怎么答第一层承认这是一个真实存在的窗口第二层说明你的对策。常用对策有这么几种使用RedLock向多个独立节点加锁部分节点挂掉不影响整体互斥使用ZooKeeper/etcd这类强一致性存储实现锁对业务做兜底比如数据库唯一键、版本号乐观锁即使锁丢了也能保证数据最终正确开启Redis的WAIT命令同步等待从节点确认但会牺牲性能。我的个人经验是绝大多数业务场景锁的互斥性被突破并不等于一定会出大事故。比如你用它做缓存重建多个线程同时回源数据库最坏情况是数据库压力增大但不会产生脏数据。真正需要绝对互斥的场景比如金融交易我建议直接从架构层面避免依赖分布式锁或者换更可靠的组件。这就是我常说的分布式锁不是银弹选型先看业务容忍度。4.3 时钟跳跃和 GC 停顿对锁的影响这个属于加分项能答上来基本就是理解型候选人了。Redis的过期时间依赖服务器本地时钟如果主节点上发生时钟跳跃比如NTP时间同步、管理员手动调时间可能导致锁提前过期或延迟过期。锁提前过期A的锁被B拿到互斥破坏锁延迟过期A释放锁后B长时间拿不到锁影响可用性。GC停顿的场景更隐蔽A持有锁业务线程进入Full GC停顿了几十秒这期间锁到期自动释放B获取锁进入临界区。A GC结束后继续执行此时A和B都在临界区里互斥被突破。由于GC停顿时间不受锁过期时间控制这个问题通过延长过期时间解决不了只能通过续期监控GC来降低概率。所以在讲锁的安全性时不要只说Redis本身要让面试官看到你有全链路视角客户端、网络、Redis、JVM任何一个环节出问题都可能影响锁的语义。能把这个问题分析到这个颗粒度面试官基本会认为你是真正处理过线上问题的人。5. 实战踩坑记录与排查思路5.1 误删锁引发的线上事故说一个我当年真实遇到的情况。团队有个定时任务每5分钟跑一次用Redis分布式锁防重。第一次上线没经验加锁用SETNX锁的value写死了字符串1释放直接DEL。结果有一次业务线程执行时间超过了锁的过期时间锁自动释放后另一个实例的线程拿到了锁两个线程同时在跑同一份任务。前一个线程执行完后DEL把后一个线程的锁删了。紧接着第三个线程又拿到锁三个线程并发跑最终导致数据库里产生了重复数据。这个事故的教训就是第3.2节说的value必须唯一释放前必须校验校验和删除必须原子。后来我补了Lua脚本并且给每个线程生成一个requestId问题才彻底解决。你把这个事故讲出来比背定义有力得多。排查这类问题有一个很实用的方法在加锁和解锁的地方打日志带上requestId、锁key、执行耗时。出问题时通过日志搜索同一个key的加锁记录按照时间线还原谁会持有锁。如果没有日志只能去Redis查TTL和当前value但历史记录已经丢了排查难度翻倍。5.2 锁粒度过大拖垮接口另一个常见坑是锁粒度。有些团队把锁key设计成order: userId :global把一个大用户的所有订单操作都锁住。这样确实避免了并发问题但一个用户的串行下单会让下单接口的RT直线上升尤其在大促场景下锁冲突会把Redis和业务线程都拖垮。我的建议是锁key尽量细粒度化。比如秒杀场景库存是按SKU维度扣减的锁key就是stock: skuId同一个用户在同一个SKU下的重复请求再叠加一个userId维度。粒度越细锁冲突概率越低吞吐量越高。分布式锁的粒度问题是面试里容易忽略但实际很要命的点你主动提出来说明你有容量规划和并发设计意识。同时还要注意锁key的设计要避免热点。比如所有请求都命中同一个商品的库存锁这个key就是热点key单分片Redis压力会非常大。解法是给锁key做分段比如把一个SKU的库存拆成10个段每段对应一个锁key先随机取段再扣减减少单key压力。当然这需要业务逻辑配合复杂度会上一个台阶但这是大促场景的常见解法。5.3 分布式锁的监控与压测不要以为上完锁就完事了。锁的监控非常重要至少要看三个指标加锁成功率、锁等待耗时、锁持有耗时。加锁成功率低说明锁冲突严重锁等待耗时高说明请求大量阻塞锁持有耗时长说明临界区代码有性能问题。我的做法是封装一层分布式锁工具类统一在加锁、解锁、获取锁失败三个节点打点上报到监控系统。日常通过大盘观察如果加锁成功率低于某个阈值比如95%就要开始查锁粒度、锁key设计、业务执行耗时。压测的时候要用一倍的线上流量去压观察锁等待曲线提前暴露锁竞争带来的RT恶化。还有一点千万不要在持有锁的代码块里做耗时太长的外部调用。比如锁住的业务逻辑里又去调第三方HTTP接口、发消息、查慢SQL这些都会让锁持有时间指数级上升。正确做法是锁保护范围内只执行必要的共享资源操作其他耗时操作挪到锁外。6. 我准备这套八股文的方法论最后分享一个我自己准备这类面试题的方法。第一步先画知识树分布式锁的考点从为什么需要展开到几种实现对比再到Redis实现细节最后到异常场景和工程权衡。先有框架再填细节不要上来就背命令。第二步每个知识点都要能讲出为什么。比如SET NX EX为什么不拆成两步释放锁为什么用Lua看门狗为什么默认30秒RedLock为什么有争议。能把为什么讲清楚才说明你真正理解死记硬背的答案是撑不过追问的。第三步做场景推演。自己假想线上发生故障锁没释放、锁误删、锁过期、主从切换然后思考怎么排查、怎么修复、怎么预防。面试官问到你项目里遇到过什么问题的时候你直接讲出来的就是真实经验而不是编的信服力完全不一样。第四步把知识点串成自己的话术对着镜子或者录音机讲一遍。分布式锁相关的知识点比较密自己讲一遍能发现很多卡壳的地方梳理完这些话术面试时的回答就会流畅很多。6.1 一份可以直接背的面试速答模板下面这份模板我整理成了几段可以直接用的口头表达你在面试前练一练会有奇效分布式锁主要用于解决多实例部署下多个进程对共享资源的互斥访问。常见实现有三种数据库锁、ZooKeeper/etcd锁、Redis锁。我们项目选型用的是Redis原因是性能高、接入成本低团队已有Redis集群。加锁用SET lock_key unique_value NX EX保证原子性value存的是当前请求的唯一ID释放锁用Lua脚本先比对再删除避免误删别人的锁。为了防止业务执行时间超过锁过期时间引入看门狗续期机制。对于主从切换丢锁的问题我们评估过业务容忍度接受这个极小概率的窗口同时通过数据库唯一键兜底。如果要绝对安全可以考虑RedLock或ZooKeeper但工程上成本和收益要权衡。这段话基本覆盖了面试官想听的所有关键点而且逻辑上是自洽的。你可以在自己的项目背景上稍微调整业务描述让内容更贴合实际。6.2 再说点题外话分布式锁这道题这几年越来越难不是因为技术本身变复杂了而是面试官越来越反感背八股。我见过太多候选人把RedLock源码背得滚瓜烂熟但问到他项目里锁key怎么设计、冲突率多少、有没有因为锁出过故障就一脸茫然。技术深度固然重要但工程经验才是真正拉开差距的地方。所以你要做的不光是看完这篇文章而是回到自己的项目里去看看你们现在的锁是怎么写的value是不是唯一的有没有看门狗锁粒度合不合理监控有没有覆盖。把这些问题真的解决了下次面试被问到分布式锁你根本不需要背因为你做过你踩过坑你自然讲得出来。这也是我最近带团队的一个体会面试题本质上是在筛选有真实经验的人而真实经验必须靠实际踩坑和复盘换取。分布式锁这个小玩意儿看着简单里面的取舍和边界恰恰是一个后端工程师并发设计能力的缩影。
返回列表