Redis红锁到底怎么解决,问题和方案其实没那么简单讲清楚
- 问答
- 2026-01-04 17:08:08
- 22
要理解红锁,得先知道它想解决什么问题,在没有红锁之前,人们常用一个Redis实例来实现分布式锁,做法很简单:客户端用一个像“set my_lock random_value nx px 30000”这样的命令,尝试在Redis里创建一个带过期时间的键值对,如果创建成功,就表示拿到了锁;如果键已存在,就表示锁被别人占着,拿锁失败,用完锁后,检查一下值是不是自己设置的那个random_value,如果是,就删除这个键,释放锁。
(来源:Redis官方文档对SET命令NX和PX参数的说明)
但这种单节点方案有个致命弱点:单点故障,如果这个唯一的Redis实例宕机了,那么整个分布式锁服务就完全不可用了,即使Redis配置了主从复制,也存在风险:客户端在主节点上成功创建了锁,但这个锁还没来得及同步到从节点,主节点就宕机了,系统切换到一个从节点作为新的主节点,但这个新的主节点上并没有刚才那个锁的记录,这时,另一个客户端过来也能成功加锁,这就导致同一个锁被两个客户端同时持有,破坏了锁的互斥性。
(来源:Martin Kleppmann的文章《How to do distributed locking》)
正是为了解决这个问题,Redis的作者Salvatore Sanfilippo提出了红锁算法,红锁的核心思想是,不再依赖一个单一的Redis实例,而是同时向一个由多个(通常是5个)独立的Redis主节点(注意,不是主从集群,这些节点是相互独立的,避免同时故障)组成的集群申请锁。

红锁算法的具体步骤大致如下:
- 获取当前时间:客户端记录开始获取锁的精确时间(以毫秒为单位)。
- 向所有节点发起加锁请求:客户端使用相同的键名和随机值,依次向那N个Redis实例发送SET命令(带NX和PX参数),为了避免某个节点响应慢而长时间阻塞,给每个请求设置一个远小于锁过期时间的网络超时时间(锁过期30秒,网络超时设5-10秒)。
- 计算获取锁耗时:客户端再次获取当前时间,减去第一步的时间,得到整个获取锁过程消耗的总时间。
- 判断是否成功:只有当客户端成功从大多数(N/2 + 1,比如5个里面成功3个)节点上获取了锁,并且总耗时没有超过锁的有效时间,才认为加锁成功。
- 如果成功:锁的实际有效时间等于最初设置的有效时间减去步骤3计算出的总耗时,这样可以补偿网络延迟带来的时间损失。
- 如果失败:客户端必须向所有Redis实例(包括那些它认为加锁成功的实例)发送删除锁的请求,即使那个实例返回了错误,这是为了清理现场,防止在部分节点上留下垃圾数据。
(来源:Redis官方文档《Distributed locks with Redis》)
你看,红锁通过“多数派”原则,大大降低了单点或少数节点故障导致锁失效的概率,要破坏红锁的互斥性,需要同时有大多数Redis实例发生故障,或者在客户端获取锁的过程中,发生了非常极端的时间跳跃,这种概率比单节点方案小得多。
红锁远非完美,它的争议非常大,数据库专家Martin Kleppmann曾发表了一篇非常有名的文章,对红锁提出了深刻的质疑,他的批评主要集中在“安全性”上,特别是锁在分布式系统中对共享资源的保护是否真的万无一失。

Martin的核心论点围绕着分布式系统的时序假设,红锁的安全性依赖于一个假设:每个Redis节点上的时间流逝速度是大体一致的(即虽然有误差,但不会发生巨大的跳跃),并且网络延迟是有上限的,但在现实的异步分布式系统中,这两个假设都可能被打破。
他举了一个经典的“锁失效”场景:
- 客户端A成功获取了红锁,这个锁在5个节点中的3个上设置成功,假设过期时间是30秒。
- 客户端A开始操作共享资源(比如修改一个文件)。
- 在操作过程中,发生了“进程暂停”,这可能是由于垃圾回收(GC),也可能是操作系统把进程挂起了,导致客户端A的线程被阻塞了很长时间,比如45秒。(来源:Martin Kleppmann对进程GC暂停的讨论)
- 在这45秒内,客户端A持有的红锁因为过期而自动释放了(30秒就过期了)。
- 客户端B此时来申请同样的锁,由于锁已经失效,它成功地从大多数节点上获取了锁。
- 客户端B开始操作同一个共享资源。
- 客户端A从暂停中恢复过来,它以为自己还持有锁(因为它不知道发生了暂停),于是继续完成它的操作,并最终释放了锁(实际上释放的是客户端B的锁),结果就是,客户端A和B同时操作了共享资源,数据可能就错乱了。
Martin认为,任何依赖超时机制的锁,都无法完全避免这种进程暂停或网络延迟带来的风险,红锁虽然通过多节点提高了可靠性,但在这个根本性的问题上,它和单节点Redis锁没有区别,他提出,要真正安全地管理共享资源,应该使用一种叫“防护令牌”的模式,简单说,就是锁服务在发放锁的同时,生成一个单调递增的令牌号,客户端在操作资源时,必须带着这个令牌号,资源服务器在允许操作前,会检查当前请求的令牌号是否比之前所有操作过的令牌号都大,否则就拒绝操作,这样,即使客户端A的锁过期了,客户端B带着新令牌来操作,资源服务器会记录下B的更大令牌号,当A带着过期的令牌试图操作时,会被资源服务器拒绝。
(来源:Martin Kleppmann文章中对防护令牌方案的描述)

Redis作者Salvatore对此进行了反驳,他认为Martin描述的场景确实存在,但属于极端情况,他指出,在大多数实际应用中,可以合理地假设进程暂停和网络延迟不会超过锁的过期时间,红锁的设计目标是在一个“存在一定故障模型,但并非最坏情况”的现实中提供一种有效的解决方案,而不是去解决所有理论上可能发生的、概率极低的问题,他认为,如果你需要应对Martin描述的那种极端恶劣环境,那么你可能根本就不应该使用基于Redis的锁,而是应该选择像ZooKeeper这样的一致性协调系统,因为ZooKeeper通过 Zab 协议能提供更强的一致性保证,但它通常性能开销更大。
(来源:Salvatore Sanfilippo对Martin Kleppmann质疑的回应)
总结一下红锁到底怎么解决以及它的问题:
- 它解决了什么:红锁主要解决了单点Redis或简单主从Redis作为分布式锁时的可靠性问题,通过部署多个独立的主节点,用“民主投票”的方式,显著降低了因单个或少数节点故障导致锁服务完全失效或锁数据丢失的概率。
- 它没解决/存在的争议:红锁无法从根本上解决由进程暂停、系统时钟严重漂移等导致的安全性问题,在这些极端但理论上可能的场景下,锁的互斥性依然可能被破坏。
是否使用红锁,取决于你的业务场景:
- 如果你的应用对锁的绝对安全性要求是极高的(比如金融扣款),并且你能接受更高的复杂度与性能损耗,那么可以考虑使用ZooKeeper或etcd等基于共识算法的系统。
- 如果你的业务场景更追求性能,并且可以容忍在极其罕见的情况下锁失效带来的风险(比如锁失效可能只是导致一次缓存重建,而不是资金损失),那么红锁在多数情况下是一个简单有效的折中方案,关键在于,你需要清楚地认识到它的能力和局限,而不是把它当作一个完美的银弹。
本文由芮以莲于2026-01-04发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/74446.html
