用Redis锁来防止缓存更新出错,保证数据不乱七八糟的安全方法
- 问答
- 2026-01-12 00:01:23
- 4
在构建使用缓存的系统时,我们经常会遇到一个经典问题:当缓存数据失效的瞬间,大量请求同时涌入,都发现缓存是空的,于是它们都会跑去数据库查询数据,然后都想把查询结果塞回缓存里,这个过程如果控制不好,就会导致数据错乱、数据库压力激增,有一种常用的、借助Redis来实现锁的方法,可以比较有效地解决这个问题,这种方法的核心思想很简单,就是只让一个请求去干重建缓存这件“苦力活”,其他请求等着它干完,然后直接享用成果,下面就来详细说说怎么安全地实现这个锁。
最关键的在于如何“占住”这个锁,我们不能简单地用Redis的SET一个键值对来表示上锁,因为万一拿到锁的请求在操作过程中挂掉了(比如所在的服务器宕机了),那么这个锁就永远无法被释放,其他请求就会永远等下去,这就是可怕的“死锁”,我们给锁设置一个“寿命”是非常重要的,在Redis中,我们可以使用SET key value NX PX milliseconds这个命令,这个命令的意思是:只有当这个key不存在时(NX选项),我才设置它的值,并且同时设置一个过期时间(PX选项),单位是毫秒,这样一来,即使拿到锁的请求中途崩溃,锁也会在设定的时间后自动消失,避免了死锁,这个value也不能是随便的值,最好是一个唯一标识,比如请求ID或者UUID,这主要是为了在解锁时确保安全。

当一个请求(我们叫它请求A)成功执行了这个SET命令后,它就认为自已拿到了锁,它就可以放心地去数据库查询数据了,而其他同时到来的请求(请求B、请求C……)在执行SET命令时,会因为NX选项(key已存在)而失败,这就意味着它们没有拿到锁,这些没拿到锁的请求该怎么办呢?它们不能无休止地反复尝试去抢锁,那样会给Redis和应用程序带来不必要的压力,常见的做法是,它们先短暂地“睡”一会儿(比如等待几毫秒或几十毫秒),然后重试,去检查缓存里是否已经有数据了,这个过程可以重复几次,但如果重试多次后发现缓存还是空的,而锁也依然存在,那可能意味着前面那个拿到锁的请求处理得非常慢或者出了什么问题,为了避免长时间等待,我们应该给这些等待的请求设置一个超时时间,超过这个时间还没拿到数据,就直接从数据库查询,作为一种降级策略,虽然可能增加数据库压力,但保证了服务的基本可用性。

请求A从数据库查到了数据,它需要把数据写入缓存,在写入之前,它必须先判断一下:我手里的这把锁还是我的吗?因为有可能它处理得太慢,以至于它之前设置的锁已经过期自动释放了,而另一个请求(比如请求B)可能已经重新建立了缓存,如果请求A不加判断就直接写入,就可能会用旧的数据覆盖掉请求B刚刚写好的新数据,导致数据错乱,安全的做法是,在写入缓存前,请求A需要验证一下锁的所有权,它可以通过Redis的GET命令获取当前锁对应的value,看看是否还是自己当初设置的那个唯一标识,如果是,说明锁还是自己的,可以安全地更新缓存;如果不是,说明锁已经易主,自己应该放弃更新,直接返回即可。
最后一步就是释放锁了,这步看似简单,但同样有坑,请求A在更新完缓存后,需要删除那个锁的key,这样其他等待的请求才能有机会去获取锁,但删除锁的时候也必须小心:绝对不能删除别人的锁!想象一下,如果请求A因为某些原因执行得很慢,它持有的锁已经超时释放了,并且请求B已经成功获取了锁,此时如果请求A最后还去执行删除锁的操作,就会把请求B的锁给删掉,这会导致第三个请求C可能趁虚而入,又引发一轮混乱,删除锁必须是“验明正身”的删除,我们不能直接用DEL key命令,而应该使用Lua脚本,因为Lua脚本在Redis中是原子性执行的,可以确保判断和删除两个动作连续完成,中间不会被其他命令插入,脚本的逻辑大概是这样的:先GET锁的value,如果获取到的value与请求A持有的唯一标识相等,那么才执行DEL操作删除锁;否则,就不做任何操作,通过这种方式,才能安全无误地释放掉只属于自已的锁。
用Redis锁来安全更新缓存,主要围绕四个要点:第一,用SET NX PX命令来获取一个有过期时间的锁,避免死锁;第二,获取锁失败的请求要有合理的等待和重试机制,并设置超时降级;第三,在更新缓存前,要再次验证锁是否还属于自己,防止覆盖他人数据;第四,使用Lua脚本原子性地验证并释放锁,确保不会误删他人的锁,只要遵循这些步骤,就能在很大程度上防止缓存更新过程中出现数据乱七八糟的情况,这种方法在《Redis实战》等书籍和许多技术博客中都有提及,是分布式系统中一种基础的同步机制。
本文由畅苗于2026-01-12发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/78987.html
