ARTICLE DETAIL

资讯详情

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

Redis锁续期机制详解:Redisson看门狗与生产避坑指南

Redis锁续期机制详解:Redisson看门狗与生产避坑指南 聊一个几乎每次面试都会被问到的问题Redis锁续期机制。说它“几乎必问”一点不夸张只要你简历上写了缓存、写了分布式面试官大概率会从你项目里的秒杀、订单幂等、任务调度切入先让你说一遍分布式锁的常规用法然后突然丢一句如果业务执行时间超过了锁的过期时间怎么办这个问题看着简单但能把原理讲透、把边界讲明白的人真不多。很多人只背了“Redisson有看门狗自动续期”这句话面试官多问两层就露馅。这篇文章不讲玄的就按我自己的理解把锁续期机制的来龙去脉拆开为什么要有续期、底层做了什么、生产环境有哪些坑以及面试官从这个点能连环追问到多深。适合正在准备面试的人也适合生产环境里用了Redis做锁、但心里一直没底的开发同学。看完之后你不只是会答一个面试题你会对整个Redis分布式锁的边界和取舍有更清楚的认识。1. 分布式锁为什么要设计“续期”这件事1.1 锁过期时间的双面性先回到最基础的场景。用Redis实现分布式锁核心就一句话在Redis里设置一个唯一key谁设置成功谁就拿到了锁用完后删除key释放锁。为了让锁不会因为持有者崩溃而永远不释放几乎所有人都会给锁设置一个过期时间。比如用SET key value EX 30 NX30秒后锁自动消失。但“过期时间”本身就是一把双刃剑。它解决了客户端宕机后锁不释放的问题却也埋下了一个隐患如果业务执行时间超过30秒锁会在业务还没结束时提前释放。锁一释放其他线程就能拿到同一把锁进入临界区原来的线程还没退出两个线程同时在做同一件事轻则重复执行重则超卖、对账错乱、数据被来回覆盖。我见过很多项目死锁和并发事故都出在这个地方。过期时间设短了业务稍微慢一点就踩雷设长了客户端崩溃后别人干等着进不去整个系统被一个死节点拖住。所以你会看到所有正经的分布式锁方案都会把“过期时间”和“业务耗时”这两个变量的关系单独拎出来处理“续期”就是为了化解这对矛盾而生的。1.2 续期的本质确认持有者还在再延长生命续期这件事翻译成大白话就是在锁快要过期的时候延长它的过期时间让业务能安全跑完。听起来简单但这里有几个关键约束。第一续期不是无脑延长必须确认当前锁的持有者还是自己。如果锁已经被别人拿到了你再跑去EXPIRE一下等于把别人的锁寿命也续上了这比不续期危害更大。所以续期操作里必须带上“持有者身份校验”确认这个锁的owner没变才允许续期。第二校验持有者和延长过期时间必须是一个原子操作。如果先查持有者、再执行过期设置中间一旦有并发插入校验就失真了。比如你用两条命令第一条查到当前锁还是你的第二条还没执行锁恰好到了被其他线程抢走了然后你的第二条EXPIRE命令就把别人的锁给续期了。这种问题在Redis客户端里特别容易犯。正确做法是用Lua脚本把“判断持有者重置过期时间”合到一次Redis调用里Redis的单线程执行模型保证了这个原子性。第三续期要区分“业务线程还活着”和“锁还没有过期”。一个合格的续期机制应该绑定持有线程的生命周期线程活着、锁还没到期就继续续线程结束了、或者线程已经退出执行逻辑了锁就应该在过期时间到来时自然消失而不是被看门狗无限续下去。你可以把锁续期理解成一个酒店房间的临时密码锁。前台办入住时设置一个有效期到点自动弹开这是兜底防止客人永远不出来。但客人还没办完事、人也在房间里前台就会每隔一段时间确认一次“客人还在不在”在的话就把房间的有效期往后拨。如果客人已经退房走人了前台自然不会再去续时密码锁到点自动失效。分布式锁的续期机制本质就是酒店前台这个“确认你在不在、在就帮你续时”的动作。1.3 不续期会怎样一次并发事故现场还原拿一个很常见的场景举例两个服务节点同时处理库存扣减。节点A先拿到锁开始执行一段耗时45秒的对账逻辑但锁的过期时间只有30秒。第30秒一到锁自动释放节点B立刻拿到锁也开始执行同样的扣减逻辑。此时A还没跑完B已经进来了两个线程同时修改同一个库存字段最终结果就是数据库里出现一个被扣了两次、或者被来回覆盖的错误值。有人会说那我把业务里的操作都做成幂等的不就行了吗这话没错幂等是最后的兜底防线但你不能把幂等当成解决一切锁问题的万能药。分布式锁的意义本来就是减少并发冲突的概率、保证关键路径的串行性。既然用了锁就应该把锁的生命周期管理好不能靠“反正幂等多执行几次也没事”来麻痹自己。续期机制就是用来缩小这个“锁提前失效”窗口的。2. 三种主流续期方案从“能用”到“靠谱”2.1 方案一把过期时间调得足够长最原始的做法是给锁设置一个足够大的过期时间大到业务基本不可能跑超时。比如业务正常情况下10秒能跑完锁的过期时间就设成60秒甚至120秒。这样锁提前失效的概率很低表面上看起来什么问题都没有。但这个方案有两个明显的问题。第一如果持有锁的节点真的崩了那这把锁最多要等60秒、120秒之后才会被释放其他请求在锁释放之前全部阻塞系统可用性被一个故障节点拖垮。第二它不是解决问题的只是降低概率。业务总有极端情况比如依赖的下游接口超时、数据库慢查询、大批量数据扫描一旦某次执行超过你预留的时间并发事故还是会照常发生而且间隔越长越难复现留给排查的时间窗也越短。所以我的建议是这个方案只能在两个场景下短暂应急——并发量极低的内部系统或者你已经做好幂等、锁只是“降低并发碰撞概率”的辅助手段。但凡你的业务对数据一致性要求高就别把宝押在“把过期时间调大”上。2.2 方案二业务线程手动续期稍微进阶一点的做法是自己在客户端里起一个定时任务每隔一段时间去给锁续期。核心逻辑就是前面说的Lua脚本先判断这把锁的持有者字段还是不是当前线程是就重新PEXPIRE不是就返回0后面不再续。这里要重点讲一个实现细节续期间隔和过期时间的关系。一般建议续期间隔取锁过期时间的1/3。比如你设置锁30秒过期那就每10秒续一次续的时候把过期时间重新拉回30秒。这样即使某一次续期请求因为网络抖动晚到了几秒锁距离真正过期也还有二十多秒的余量不容易出现“续期还没到锁先过期”的真空窗口。如果你把续期间隔设成30秒、过期时间也设成30秒那一旦定时任务稍微卡一下锁就已经先释放了续期等于白做。手动续期方案最大的问题是你无法保证定时任务一定活着。如果业务线程本身没挂但它所在的进程发生长时间GC停顿、或者定时任务线程池被占满、被OOM Kill续期任务就可能停滞锁照样会提前释放。更麻烦的是你很难把“业务逻辑真的还在执行”这个状态准确传递给续期任务。有些任务表面上看线程还活着实际上已经卡在一个错误分支里出不来了此时续期反而是在给一个僵尸业务“续命”。所以手动续期比“调大过期时间”靠谱但它还是治标不治本。如果你要自己做一套分布式锁组件这个方案可以作为看门狗机制的雏形面试官让你手写续期逻辑的时候画这个Lua脚本的思路就够了。但生产环境直接自己造这种轮子后续踩坑的成本会很高。2.3 方案三Redisson的看门狗Watchdog自动续期这才是工业界最主流的方案也是面试题里“锁续期机制”的标准答案背景。Redisson在加锁的时候如果你没有显式指定leaseTime它默认会启用看门狗机制锁默认30秒过期看门狗每10秒自动续期一次把过期时间重新拨回30秒一直续到业务线程退出、锁被释放为止。看门狗这个名字起得特别形象——就像一条蹲在门口盯着锁的狗时间快到了就起来叫一声“人还在”然后继续趴着。底层实现用到了Netty的定时任务调度器每次续期成功之后会注册下一轮续期任务形成一个链式循环。注意它不是用一个while(true)死循环反复续期而是靠事件驱动的定时器只有到期前才触发一次续期动作空闲时间不占用CPU和网络请求。续期的Lua脚本其实非常简单核心逻辑可以用下面这段来理解-- 简化版续期脚本 if redis.call(hexists, KEYS[1], ARGV[2]) 1 then -- 持有者还是当前线程重置过期时间 return redis.call(pexpire, KEYS[1], ARGV[1]) end -- 持有者已经变了不再续期 return 0这里有一个非常关键的细节Redisson的锁结构不是一个简单的字符串key而是用Hash结构存储的。key是锁的名称Hash里的field是持有者标识通常是UUID 线程IDvalue是重入次数。持有者标识不只是为了续期更是为了在释放锁的时候能准确判断“这个锁是不是我持有的我有没有资格删它”。如果你不用持有者标识直接DEL一把锁锁过期之后被别的线程拿到之后再释放就会把别人的锁误删掉这是一类特别经典的线上事故。还要纠正一个很多人理解反了的点看门狗不是无脑续期的永动机。它只在持有锁的线程还活着的时候续期线程一旦退出、或者客户端和Redis连接断了看门狗任务会被同步清理掉锁会在剩余过期时间到达后自然释放。换句话说看门狗解决的是“业务正常执行但时间不够”的问题而不是“锁应该永远不释放”的问题。另外如果你手动指定了leaseTimeRedisson会自动关闭看门狗机制。这是一个很反直觉的坑很多开发者在调用lock.lock(10, TimeUnit.SECONDS)之后以为还有看门狗兜底实际上这种显式超时模式下锁到期直接释放没有任何续期。所以用Redisson的默认无参lock()方法时要清楚你依赖的是看门狗的默认30秒值用带超时参数的lock()时要接受锁到期就消失的设定。2.4 三种方案横向对比实现方式是否感知持有线程存活异常时兜底表现推荐程度过期时间调大否客户端崩溃时锁被占用时间很长低并发场景临时用手动续期任务定期执行无法感知业务真实状态定时任务崩溃时锁提前失效自研锁组件可用Redisson看门狗是锁绑定线程生命周期线程退出后锁到期自动释放生产环境首选我自己在项目里只用Redisson看门狗的时候还会额外做一件事把锁的过期时间调成业务耗时的至少三到五倍而不是图省事用默认30秒硬扛。比如业务P99是5秒就把lockWatchdogTimeout配成30秒甚至60秒。这样既不会让续期请求发得太频繁又能在极端情况下留出足够的容错空间。3. 面试官真正想考的从续期延伸到分布式锁的边界3.1 续期只能解决“时间不够”这一件事面试官如果只想知道怎么续期问一句“业务超过锁过期时间怎么办”就够了。之所以追着这个话题连环问是因为续期这个点能很自然地引出分布式锁的完整边界。你要清楚续期只能解决“锁还在、但快到期了”的问题它解决不了锁丢失、Redis故障、主从切换、网络分区这些问题。最典型的场景是主从切换。节点A持有Redis锁写入了master节点。结果master还没来得及把锁记录同步到slave就宕机了哨兵把slave提升为新的master而新master上根本没有A的锁记录。这时候节点B来加锁轻松成功。于是A和B同时以为自己持有锁同时执行同一段业务这就是分布式锁的“脑裂”。看门狗此时毫无办法因为锁的底层数据都已经没了续期脚本再跑也查不到持有者字段。这也是为什么面试聊到续期经常会顺势问到RedLock或“为什么不用数据库锁”。因为Redis分布式锁本就带有一定的“概率性安全”特征续期机制只是让它在正常场景下更稳并不能把它变成强一致方案。你能主动说出这个边界面试官马上就觉得你不是只会背API。3.2 RedLock与续期要求RedLock是Redis作者提出的一种算法在N个完全独立的Redis节点上同时加锁只有成功加锁的节点数超过N/2才认为加锁成功释放锁时对每个节点都发送删除命令。它的设计目标就是降低单个master宕机导致锁丢失的概率。但RedLock对续期的要求比单节点锁更高。因为一把锁分布在多个节点上续期的时候必须对每个已加锁成功的节点都续期任何一个节点上的锁提前过期整个锁的“有效重叠时间”就可能断裂。分布式系统里还存在时钟漂移一个节点上的过期时间可能因为时钟偏差而提前或延后所以RedLock论文里专门强调锁的有效时间必须设置得足够长要扣除操作耗时、节点不可用时间和时钟漂移量的总和否则锁可能在所有节点上同时失效。这个数学前提面试时不用展开推导但你要能说出“有效时间必须大于重叠窗”这个意思。面试管通常会顺着问RedLock还有争议你怎么看这时候你只要说“它把单点问题摊薄了但没有彻底解决而且还牺牲了可用性很多场景下用合理配置的单机锁加续期就够了”就已经超过绝大多数候选人。3.3 为什么MySQL方案不需要“续期”还有一条对比线MySQL的分布式锁为什么没有“续期”这个概念。数据库方案通常是利用唯一索引或行锁一个事务里执行SELECT ... FOR UPDATE事务提交或回滚时锁自然释放事务本身有超时控制事务执行时间由数据库统一管理。不存在“业务线程跟数据库连接断开但事务还挂着”这种语义。好处是强一致、没有Redis主从切换丢锁的脑裂风险代价是性能天花板低、数据库压力大、并发上去后容易成为瓶颈。很多老系统用数据库锁用得挺好因为它们本身不追求高并发的锁操作。面试官问这个对比考察的是你对技术选型的理解能说出“锁的到期释放机制取决于底层存储的语义Redis需要续期是因为它的key天然带TTL而数据库事务天然有生命周期”会非常加分。3.4 续期之外重入、阻塞等待、公平性既然聊到Redisson面试官大概率还会顺手问几个衍生的语义锁支持重入吗重入次数存在哪线程拿不到锁时是怎么等待的这些看起来是Redisson的功能点其实都在考验你对分布式锁“完整语义”的了解。重入的实现依赖Hash结构同一线程重复加锁时不再创建一个新key而是把Hash里own线程对应的value加1。释放锁时减1减到0才真正删除key。这个设计跟JUC里ReentrantLock的思路是一致的只是从内存搬到了Redis。线程拿不到锁时会怎么等Redisson内部通过一个信号量和Redis的Pub/Sub订阅机制配合拿不到锁的线程先订阅一个释放消息通道然后阻塞等待信号量持锁线程释放锁时解锁脚本会PUBLISH一条消息唤醒等待中的线程。这样就不会用“轮询间隔几毫秒再试一次”的方式空转减少了对Redis的无意义请求。这些细节都能看出一个功能完整的分布式锁单靠Redis本身是做不到的Redis只是提供了最底层的原子写能力其余语义全是客户端“外挂”出来的。你把这条逻辑讲清楚就回答了“为什么建议直接使用成熟组件而不是自己写锁”。4. 生产环境里续期机制的坑与排查经历4.1 GC停顿导致看门狗“失职”看门狗也不是万能的它有一个很隐蔽的失效场景你的应用进程长时间GC停顿。想象一下业务线程正在执行业务JVM突然发生了一次Full GC整个进程暂停了十几秒期间什么定时任务都执行不了包括看门狗的续期动作。等GC结束、进程恢复锁可能早在暂停期间就过期被其他节点拿走了而业务线程自己还不知道继续闷头执行到最后然后数据库里已经被两个线程同时写过一遍。这种事故特别坑人。因为你查日志的时候看不到任何“续期失败”的报错看门狗压根没有执行而不是执行了失败。排查方向会一直集中在业务代码和Redis之间绕圈。我后来总结的排查方法是把业务在锁内耗时、锁的TTL变化、GC停顿日志放在一起看。一旦发现某个时间窗口内锁提前释放了同时GC日志里有长停顿基本就能对上号。应对办法有两层第一层是优化应用GC尽量消除秒级停顿第二层是不要在业务里完全仰仗续期在关键数据写入之前用一个原子脚本“检查锁持有者重新确认自己还有锁”确认失败就走降级或报错。续期是概率性的保护业务侧的最终一致性校验才是底裤。4.2 Redis连接超时引发的“续期链断裂”还有一类常见故障是网络抖动导致Redis连接超时看门狗连续几次续期请求都发不出去最终锁提前失效。这个跟GC停顿的差别在于它是Redis客户端层面的问题日志里会有RedisCommandTimeoutException之类的报错。处理思路有几个一是把Redis客户端配置里的超时时间调到一个合理值不要用默认的特别短或特别长的值二是把lockWatchdogTimeout调大给网络抖动留出更多缓冲比如默认30秒改成60秒续期周期也会跟着变成20秒一间给网络留出余量三是加监控对锁的TTL做实时观测锁的剩余时间一旦低于阈值就告警这样即使续期断了你也能在业务还没跑完之前人工介入。我自己习惯在生产环境给锁相关Redis操作单独建一个连接池而不是和普通缓存读写混用一个连接池。因为缓存读写本身对超时不敏感而锁续期属于“关键时刻不能掉链子”的操作混在一个池子里容易被其他线程的慢查询拖垮。4.3 时钟跳跃对续期的影响Redis的EXPIRE和PEXPIRE依据的是Redis服务器本地时间。如果Redis所在的主机发生时钟跳跃锁的过期时间计算也会受影响。时间向后调锁的实际存活时间会变长顶多让其它线程多等一会儿时间向前调锁可能提前过期被其它线程拿到锁并发事故随之触发。生产环境通常会开NTP做时钟同步但NTP本身有一种粗暴调整模式会直接跳变时钟而不是逐渐微调。对依赖锁等强时序语义的系统来说时钟回拨几十毫秒都可能出问题。我建议在Redis服务器上关闭NTP的跳变同步改成渐进式调整如果用的是云厂商的托管Redis至少要确认它的时间同步策略是否会发生大跨度回拨。虽然这个细节不在面试题的常规范围里但一旦你在生产环境被它坑过一次就会明白分布式系统里“时间”这种东西从来不是绝对可信的。4.4 “续期永动机”陷阱最后一个坑比较反直觉看门狗把锁续得太久反而成了事故。我之前接手过一个跑批系统业务线程因为一个内部循环任务没有正常退出一直在执行一段已经失去意义的逻辑看门狗就跟着一直续期锁挂了一个多月导致所有依赖这把锁的流程全部阻塞。最后排查出来问题不在锁而在业务代码逻辑但锁的“无限续期”掩盖了这个问题让故障一直在暗处发酵直到业务方开始报“流程一直卡住”才暴露。这里的本质是续期机制默认“只要线程活着就续”它不知道线程是在做有意义的事情还是已经卡死。所以你在使用分布式锁的时候心里要有界限锁是用来保护“临界区”的不是用来包裹整个批量任务的。如果业务真的很长应该考虑把它拆成分段任务、用流程编排替代一把锁从头持到尾或者给锁设置一个绝对过期上限超过这个上限就强制让锁失效并告警。让分布式锁永远续期等于把整个系统的可用性托付给业务代码百分之百没有死循环。5. 面试现场一套让面试官点头的作答框架5.1 先给结论再拆原理如果你在面试中遇到“Redis锁续期机制”这个问题我建议你第一句话就给出一个可以直接记录的结论式回答锁设置过期时间是为了兜底防止持有者崩溃后锁永远不释放但过期时间可能小于业务耗时导致锁提前失效续期的本质是确认锁持有者还在然后原子性地延长过期时间生产环境常用Redisson的看门狗机制默认30秒过期、每10秒续期一次底层通过Hash结构和Lua脚本保证“持有者校验续期”的原子性线程退出后锁会自然过期释放。这段话只需要二十秒但信息密度很高。面试官一听就知道你理解的是完整链路而不是背过答案。之后他再往细节问你只需要在刚才的框架里展开。5.2 主动暴露边界说完结论后最好主动补一句续期机制只解决锁提前失效的问题它解决不了主从切换导致锁丢失、Redis节点故障导致的不可用如果业务对一致性要求极高需要单独评估RedLock或数据库方案。这句话相当于抛了一个话题钩子让面试官从“背题模式”切换到“讨论模式”。他要么追着RedLock问要么追着主从切换问不管哪个方向你都能顺势展示自己做过深入思考。千万不要只说“我们用的是Redisson它有看门狗自动续期”就结束了。这种回答等于把题目的所有深度都堵死了面试官只能认为你从来没有真正看过源码只是用过框架。以现在大厂的面试习惯问到第三方组件机制类问题几乎都会考察“用得清不清楚、边界在哪儿、有没有自己的判断”。5.3 连环追问与回答思路我把面试官常追问的几个问题和对应的回答方向整理成了表格你可以对照着查漏补缺。面试官追问回答方向看门狗续期失败会怎样锁会在过期时间到达后释放业务可能在无锁状态下继续运行需要业务侧做兜底校验为什么不在一个线程里循环续期死循环会持续占用CPU和网络链式定时器只在到期前触发更高效也更安全自己实现续期逻辑怎么做用Hash存持有者标识用Lua脚本完成“hexists校验pexpire续期”返回0就停止续期锁的持有者标识为什么必须有防止锁过期被其他线程持有后原线程误删别人的锁也防止无身份限制的误续期加锁为什么要用Lua/SET原子命令因为SETNX和EXPIRE分开执行Redis宕机或网络抖动时可能在中间断开导致锁没有过期时间变成永久死锁你能把这些串起来其实已经覆盖了这道题的绝大多数得分点。5.4 用一个小类比收尾在面试场景里类比可以让你的回答更有记忆点。前面提到的“酒店临时密码锁前台定期确认续期”就很好用。你可以在最后总结的时候说分布式锁的续期机制本质上就是酒店前台每隔一段时间去确认客人还在不在房间在的话就帮他把房间有效期往后拨客人退房走了就停止续期、让房间到点自动释放。面试官记不住你背了多少细节但会记住这个画面感极强的答案。写在最后回到我自己的经历。早期项目里我曾经自己写过一把Redis锁续期就用一个单独的定时任务线程去做结果线上出过一次“幽灵锁”业务线程已经退出了定时任务却还在傻傻续期导致其它服务整整阻塞了一个多小时。后来切到Redisson的看门狗机制又踩过一次GC停顿导致锁提前失效的坑。这些教训让我明白一件事锁续期不是加一个定时器那么简单它背后的问题是“如何准确判断一个分布式线程是不是真的活着”这件事没有绝对正确答案只能靠成熟的机制和经验不断逼近。如果你正在准备面试我的建议是花一个下午把Redisson的加锁、续期、解锁三块源码通读一遍重点看Hash结构和Lua脚本如果只是想在生产上少出事故记住一个最简单的原则锁的过期时间永远不要按平均值设按P99耗时乘以三以上留余量并给续期失败配好告警。下一篇如果你们想看我再把Redisson的锁释放和等待唤醒机制单独拆开聊聊。
返回列表