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

Redis用链表搞排它锁的思路和那些细节你知道吗?

这个思路的核心,并不是Redis官方提供的一个标准功能,而是开发者利用Redis的数据结构和命令,特别是链表(List)和相关的原子操作,自己构建一种锁机制,它和我们常说的用SETNX命令实现的分布式锁是两种不同的实现路径,这种链表锁的思路,其灵感或参考来源可以追溯到一些技术社区(如早期的博客园、CSDN上的技术分享)或者开发者对于分布式系统原语的深入探讨,其本质是模仿现实世界中“排队”的概念。

核心思路:排队拿锁

想象一下现实生活中的场景:只有一个卫生间的合租房,大家怎么公平地使用?通常的做法是排队,第一个到的人进去锁门,后面来的人依次排在后面等待,里面的人出来了,就通知下一个排队的人。

Redis用链表搞排它锁的思路和那些细节你知道吗?

Redis链表锁就是模拟这个过程,它的基本想法是:

  1. 创建一个队列:在Redis中,用一个List数据结构来代表等待锁的队列,这个List的key可以叫做 lock_queue:[锁名称]
  2. 排队获取资格:当一个客户端想要获取锁时,它不像SETNX那样直接去抢,而是“礼貌地”使用 LPUSHRPUSH 命令,将自己的唯一标识(比如一个UUID)放入这个队列的末尾,这一步相当于去排队领个号。
  3. 检查自己是否排第一:放入自己的标识后,客户端立即使用 LINDEX lock_queue 0 命令(或者 LRANGE)来查看队列的第一个元素(即队首)是不是自己的标识。
  4. 是队首则获得锁:如果检查发现队首就是自己的标识,恭喜你,你排在最前面,锁目前是空闲的,你可以立即获取锁并执行业务逻辑。
  5. 不是队首则等待:如果队首是别人的标识,说明已经有人在排队了,你还没有拿到锁,客户端不能干等着,它需要进入一个等待状态,并设法监听队列的变化。

关键的细节与挑战

Redis用链表搞排它锁的思路和那些细节你知道吗?

上面的步骤1-4听起来很简单,但真正让这个锁变得可靠、可用,需要解决以下几个关键的细节问题,这也是这种实现方式的复杂之处:

如何可靠地“排队”?——防止重复排队和网络问题。 一个客户端在同一个锁请求中,应该只允许排队一次,否则,如果因为网络延迟,客户端重复执行了 LPUSH 操作,它就会在队列中占据多个位置,这破坏了公平性,在客户端代码层面需要有一个状态标记,确保只排队一次。LPUSH 操作本身是原子的,Redis保证其执行不会被打断,这是基础。

Redis用链表搞排它锁的思路和那些细节你知道吗?

如何高效地“等待和通知”?——避免轮询,使用发布订阅。 这是最核心的细节,如果客户端发现自己不是队首,最简单的做法是“轮询”(polling),即隔一秒就执行一次 LINDEX 命令检查自己是不是变成队首了,但这非常低效,会给Redis带来不必要的压力。 更优雅的做法是结合Redis的发布订阅(Pub/Sub) 机制,思路如下:

  • 每个客户端在排队时,除了放入自己的标识符(如UUID_A),还会订阅一个特定的频道,这个频道名可以和它的标识符绑定,lock_release:UUID_A
  • 当持有锁的客户端(比如UUID_B)释放锁时,它的操作顺序是:
    1. 从队列中原子性地移除自己的标识符UUID_B(使用 LREM 命令)。
    2. 移除后,队列就有了新队首(比如变成了UUID_A),释放锁的客户端需要向新队首客户端对应的频道(即 lock_release:UUID_A)发布一条消息,通知它:“嘿,轮到你了!”
  • 正在等待的UUID_A客户端收到这个消息后,就知道自己可能成为队首了,于是再次执行 LINDEX 确认,如果确认自己是队首,则获取锁。

释放锁的原子性和安全性。 释放锁的操作必须是原子的和安全的。

  • 原子性:“从队列中移除自己”和“通知下一个”这两个操作,在Redis中不是原子的,这中间可能会发生故障,导致只做了移除,没发通知,从而使队列“死锁”(后面的人永远等不到通知),为了解决这个问题,通常需要Lua脚本,用一个Lua脚本原子性地执行 LREM 移除当前持有者,LINDEX 0 获取新队首,最后再向新队首的频道发布消息,Lua脚本能确保这一系列操作作为一个整体执行。
  • 安全性:只有锁的持有者才能释放锁,在移除队列元素时,必须验证要移除的标识符确实是当前客户端自己的,防止误删他人的标识。

处理客户端崩溃——锁的释放与清理。 这是所有分布式锁都要面对的经典问题,如果某个客户端在持有锁期间崩溃了,它就无法正常执行释放锁的流程(移除队列元素+通知下一个),导致队列卡死。 对于链表锁,解决方案通常是给锁设置一个超时时间,但这不像SETNX的过期时间那么简单,一种做法是:

  • 在客户端获取锁后,启动一个看门狗(watchdog)线程,定期去“续约”自己的位置,比如每隔一段时间把自己的标识符在队列中的位置刷新到队首(这需要更复杂的列表操作,可能不适用)或者简单地设置一个键值对作为健康检查。
  • 更常见的、也是更简单的思路是,依赖一个独立的监控进程或使用Redis的键空间通知功能,监控进程可以定期扫描那些持有锁时间过长的客户端,如果判定其已死亡(比如通过心跳检测),则强制将其从队列中移除,并通知下一个客户端,但这增加了系统的复杂性。

优缺点

  • 优点
    • 公平性:严格遵循先来后到的FIFO顺序,避免了SETNX锁可能出现的“饥饿”现象。
    • 思路直观:模拟现实排队,容易理解。
  • 缺点
    • 复杂性高:需要自己处理排队、通知、原子性、异常情况等大量细节,代码实现比SETNX复杂得多。
    • 可靠性挑战:客户端崩溃后的清理机制实现起来比较棘手,不如SETNX的自动过期简单可靠。
    • 性能开销:相比一次SETNX命令,链表锁涉及多次命令交互(LPUSH, LINDEX, 订阅,发布等),尤其是在高并发下,频繁的Pub/Sub消息可能带来压力。

正是因为这些缺点和实现的复杂性,在实践中,绝大多数场景下,人们会更倾向于使用基于 SET resource_name random_value NX PX timeout 命令的分布式锁方案(或其变种,如Redlock算法),因为它更简单、更高效,而Redis链表锁更多是作为一种体现分布式系统设计思想的学术性或探索性的实现。