当前位置:首页 > 问答 > 正文

Redis锁过期了咋办?实践中遇到的问题和各种折腾的解决办法分享

这个关于Redis锁过期的问题,其实是分布式锁实践中一个非常经典又让人头疼的坑,我根据一些技术社区的分享,比如知乎上的一些高赞回答、掘金上的技术文章以及一些公司的技术博客,来聊聊大家遇到的情况和想出的各种办法。

核心问题:锁为啥会过期?

得明白锁设置过期时间是为了兜底,一个服务实例抢到锁后,还没来得及释放锁,自己就挂掉了,如果没有过期时间,那这把锁就永远不被释放,其他服务再也拿不到锁,整个系统就“死锁”了,设置一个比如30秒的过期时间,是必须的安全措施。

但问题就出在这个“兜底”时间上,实践中遇到的麻烦大致可以归为两类:

第一类:任务没做完,锁先过期了。

这是最常见的情况,你写代码的时候,觉得这个任务最多10秒就能跑完,于是给锁设置了30秒过期,心想绰绰有余,但人算不如天算:

  • 网络抖动:任务中需要调用其他服务,结果这次响应特别慢,卡了20秒。
  • Full GC:你的Java应用突然来了次Full GC,“世界暂停”了十几秒。
  • 数据库慢查询:处理过程中,某个数据库查询突然变慢。
  • 任务本身变复杂:随着业务发展,任务要处理的数据量变大了,执行时间自然变长。

结果就是,锁在第30秒的时候自动失效了,而此时,你的任务还在吭哧吭哧地执行,紧接着,另一个服务实例看到锁没了,立马自己加锁成功,也开始执行同一个任务,这样一来,就会出现两个实例同时在操作同一份数据,脏数据、重复扣款啥的乱七八糟的问题就全来了。

第二类:释放了别人的锁。

这个更诡异,接着上面的场景说:第一个实例A的锁过期后,实例B拿到了锁,实例A终于磨磨蹭蹭地把任务执行完了!它很“自觉”地去执行释放锁的代码,问题在于,它释放的是实例B刚创建的新锁!这样一来,实例B的锁被意外释放,实例C又能趁虚而入……系统就乱套了。

大家折腾出来的解决办法:

面对这些问题,社区里提出了各种方案,一个比一个“精巧”,也一个比一个复杂。

  1. “笨”办法:把过期时间设置得足够长。 这是最直接的想法,评估一下任务的极限时间,比如最长可能跑5分钟,那就把锁过期时间设为10分钟,这个方法简单有效,对于大多数业务场景来说,其实够用了,它的缺点是如果服务真宕机了,其他服务最多要等10分钟才能继续,恢复时间有点长。

  2. “续命”大法(锁续期): 这是目前最主流的解决方案,思路是:既然怕任务没做完锁就过期,那我就在锁快要过期的时候,给它“续命”,延长过期时间,具体实现就是,在获取锁成功后,再启动一个额外的线程(也叫看门狗线程),这个线程定期(比如每隔过期时间的1/3时间)去检查一下锁是否还存在,如果还存在且是当前实例持有的,就刷新一下过期时间。 像Redisson这个流行的Redis客户端,就内置了这个功能,你不用自己操心,它会在后台默默帮你续期,只要你的服务没挂,锁就一直在,这办法很好,但引入了额外的复杂性,需要维护那个续期线程。

  3. “验明正身”再释放(Value值校验): 这个方法用来解决“释放别人锁”的问题,思路是,在设置锁的时候,value不要简单地设为1或true,而是设为一个唯一的值,比如UUID或者请求ID,在释放锁的时候,先检查一下当前锁的value值是不是自己当初设置的那个值,如果是,才允许释放;如果不是,说明锁已经不属于自己了,就不能释放。 这相当于给锁配了一把“钥匙”,只有拿对钥匙的人才能开锁,这是一个非常好的安全实践,应该成为使用Redis锁的标准动作,但它只解决了释放的问题,没有解决锁过期后任务还在执行的根本问题。

  4. “优雅停机”: 这是一个辅助性的最佳实践,在应用程序收到终止信号(比如kill命令)时,不应该立刻死掉,而应该先执行一段关闭钩子(Shutdown Hook),在这个钩子函数里,去完成手头正在执行的任务,并且安全地释放自己持有的锁,这样可以最大程度避免任务执行到一半被中断,导致数据不一致,但这只能应对计划内的停机,对付不了服务突然崩溃。

  5. 转向更复杂的方案: 当业务对分布式锁的要求非常高,容不得半点闪失时,大家最终可能会选择放弃Redis,转而使用专门为分布式协调设计的系统,比如ZooKeeper或etcd,这些系统通过一致性协议能提供更强的安全性,但代价是部署和运维更复杂,性能通常也不如Redis。

Redis锁过期问题没有一劳永逸的银弹,在实践中,往往是组合拳:使用Redisson这样的客户端自动续期 + 设置合理的超时时间 + 释放锁时校验value值 + 实现优雅停机,这样能在复杂度和可靠性之间取得一个比较好的平衡,如果业务真的要求100%绝对可靠,那可能就得考虑更重的解决方案了。

Redis锁过期了咋办?实践中遇到的问题和各种折腾的解决办法分享