“多数派”的智慧:Redis Redlock 分布式锁
1. Redlock 是什么?(概念篇)
通俗理解:多数人说了算的安全锁
想象一下,您有一份非常重要的文件(共享资源),需要确保在任何时刻都只有一个人能修改它(互斥)。这份文件放在一个由 5 个保险箱(Redis 实例)组成的大金库里。
-
单机锁 (SET NX PX): 如果您只用其中一个保险箱来做锁,一旦这个保险箱突然坏了,持有钥匙的人可能永远拿不回来,或者别人误以为钥匙丢了,又造了一把新的,锁就失效了。
-
Redlock (多数派锁): Redlock 的做法是:您要拿到 3 把以上(多数派) 保险箱的钥匙,才能算真正获得了这把锁。即使其中 1~2 个保险箱出了故障,只要您还能从大多数(3 个)保险箱那里证明您持有“锁凭证”,您的锁就是安全有效的。
核心思想: 不依赖单一节点的可靠性,而是依赖大多数节点达成共识。
学术定义:多实例的分布式锁算法
Redlock 是基于 N 个 独立的 Redis 主节点来构建分布式锁的算法。
- 目标: 在分布式环境中实现互斥(Mutual Exclusion)和活性(Liveness)。
- 方法: 客户端尝试在 大多数 (Majority) Redis 实例上获取锁,并确保这些操作在非常相似的时间内完成。
- 安全性提升: 它解决了单机锁在 Redis 主从切换、宕机或网络分区时,锁可能被多个客户端同时持有的安全隐患。
2. 为什么需要 Redlock?(问题篇)
单机锁的不足与挑战
传统的 Redis 分布式锁通常依赖于一个命令:
SET key value NX PX milliseconds\text{SET} \ \text{key} \ \text{value} \ \text{NX} \ \text{PX} \ \text{milliseconds}SET key value NX PX milliseconds
这在单机模式下运行良好,但一旦引入高可用性(主从复制),问题就出现了:
挑战 1:主从异步复制导致的锁失效(安全漏洞)
- A 客户端在 Master 上成功设置了锁 L\text{L}L。
- Master\text{Master}Master 在将 L\text{L}L 同步给 Slave 之前宕机了。
- Slave\text{Slave}Slave 被提升为新的 Master\text{Master}Master。此时,新的 Master\text{Master}Master 不包含锁 L\text{L}L 的信息。
- B 客户端在新的 Master\text{Master}Master 上成功获取了同一个锁 L\text{L}L。
- 结果: 锁被 A\text{A}A 和 B\text{B}B 两个客户端同时持有,互斥性被破坏!
Redlock 的诞生就是为了解决这个致命的安全问题。通过要求客户端在 N/2 + 1 个独立节点上都成功设置锁,即使某个节点宕机并丢失了锁信息,只要大多数节点上的锁仍然存在,整个锁就是有效的,确保了安全性。
3. Redlock 怎么做?(解剖篇)
Redlock 算法要求部署 N 个 完全独立的 Redis 主节点(通常 N=5 )。以下是获取和释放锁的核心步骤,我们将结合实现逻辑来分析。
3.1 获取锁(Acquiring the Lock)
客户端执行以下步骤来获取锁:
步骤 1:时间戳记录
客户端记录当前时间 TstartT_{\text{start}}Tstart (毫秒)。
步骤 2:并行获取锁 (SET NX PX)
客户端尝试以串行或并行方式,依次向 NNN 个独立的 Redis 实例发送获取锁的命令。
SET resource_name random_value NX PX lock_validity_time\text{SET} \ \text{resource\_name} \ \text{random\_value} \ \text{NX} \ \text{PX} \ \text{lock\_validity\_time}SET resource_name random_value NX PX lock_validity_time
resource_name
: 要锁定的资源键名。random_value
: 随机字符串,作为锁的“签名”,必须是全局唯一的。这是释放锁的关键凭证,防止 A 客户端释放了 B 客户端的锁(误释放)。lock_validity_time
: 锁的有效时间。这不仅是 TTL\text{TTL}TTL,也是获取锁成功的判断依据。
步骤 3:计算时间差与判断多数派
客户端计算从开始获取到成功在至少 N/2+1\text{N}/2 + 1N/2+1 个实例上设置锁所花费的时间 TcostT_{\text{cost}}Tcost:
Tcost=CurrentTime−TstartT_{\text{cost}} = \text{CurrentTime} - T_{\text{start}}Tcost=CurrentTime−Tstart
判断成功的两个关键条件:
- 多数派成功: 客户端在 NNN 个实例中的 ≥N2+1\ge \frac{N}{2} + 1≥2N+1 个实例上成功获取了锁。
- 时间有效性: 总耗时 TcostT_{\text{cost}}Tcost 必须小于锁的有效时间 lock_validity_time\text{lock\_validity\_time}lock_validity_time。
如果满足以上两个条件,客户端认为获取锁成功。
步骤 4:计算最终有效时间
一旦获取锁成功,锁的真正有效时间是:
Final_TTL=lock_validity_time−Tcost−Clock_Drift\text{Final\_TTL} = \text{lock\_validity\_time} - T_{\text{cost}} - \text{Clock\_Drift}Final_TTL=lock_validity_time−Tcost−Clock_Drift
- 为什么减去 TcostT_{\text{cost}}Tcost? 因为在 Redis\text{Redis}Redis 实例中,锁的 TTL\text{TTL}TTL 是从 SET\text{SET}SET 命令执行那一刻开始计时的。客户端花费的时间越久,锁在 Redis\text{Redis}Redis 节点上剩下的时间就越少。Redlock 必须确保客户端拿到锁后,它在 Redis\text{Redis}Redis 上的剩余时间仍然有意义。
- 为什么减去 Clock_Drift\text{Clock\_Drift}Clock_Drift (时钟漂移)? 这是一个额外的安全裕度,用于抵消不同服务器时钟不同步带来的风险。
如果最终 Final_TTL≤0\text{Final\_TTL} \le 0Final_TTL≤0,说明耗时太长,锁已经失效了,客户端应该认为获取锁失败,并立即进入释放锁的流程。
步骤 5:失败处理
如果获取锁失败(未达到多数派,或 Final_TTL≤0\text{Final\_TTL} \le 0Final_TTL≤0),客户端必须立即向所有实例(包括获取成功的那些)发送释放锁的命令(步骤 3.2),以防止“半锁”状态。
3.2 释放锁(Releasing the Lock)
释放锁相对简单,但必须确保安全性。客户端需要向所有 NNN 个 Redis\text{Redis}Redis 实例发送 Lua\text{Lua}Lua 脚本来释放锁。
源码逻辑分析:为什么用 Lua 脚本?
释放锁的逻辑是原子操作,核心代码如下(抽象 Lua\text{Lua}Lua 脚本):
-- KEYS[1] 是资源的key
-- ARGV[1] 是客户端随机生成的 unique_value
if redis.call("GET", KEYS[1]) == ARGV[1] then-- 只有当存储的值等于客户端的 unique_value 时,才删除 keyreturn redis.call("DEL", KEYS[1])
else-- 值不匹配,不执行删除操作return 0
end
为什么必须检查 unique_value
?
这是为了防止 A 释放 B 的锁(误释放):
- 客户端 A 获取了锁,设置 TTL=10s\text{TTL}=10sTTL=10s, value=A_random\text{value}=\text{A\_random}value=A_random。
- A 业务处理时间过长(例如 20s),锁自动过期。
- 客户端 B 在 15s 时成功获取了锁,设置 TTL=10s\text{TTL}=10sTTL=10s, value=B_random\text{value}=\text{B\_random}value=B_random。
- A 客户端在 20s 时执行完业务,尝试释放锁。
- 如果不用 Lua\text{Lua}Lua 脚本检查 value\text{value}value,A 就会误删 B 设置的锁。
- 使用 Lua\text{Lua}Lua 脚本,A 释放时发现 GET\text{GET}GET 到的值是 B_random≠A_random\text{B\_random} \neq \text{A\_random}B_random=A_random,释放失败,保证了 B 的锁的安全性。
释放流程: 客户端向所有 NNN 个实例发送这个 Lua\text{Lua}Lua 脚本,无论在这些节点上是否成功获取了锁(防止异常中断留下的残余锁)。
总结:Redlock 的本质与乐趣
Redlock 算法的乐趣在于,它没有引入复杂的分布式协调服务(如 Zookeeper 或 Consul),而是巧妙地利用了 Redis\text{Redis}Redis 单个实例的原子性 (SET NX PX) 和 多数派思想来对抗分布式环境中的网络分区和节点宕机的风险。
- 是什么: 基于 N\text{N}N 个独立 Redis\text{Redis}Redis 节点的多数派分布式锁算法。
- 为什么: 解决单 Redis\text{Redis}Redis 实例在主从切换时,锁可能被多客户端同时持有的安全漏洞。
- 怎么做:
- 在 ≥N/2+1\ge \text{N}/2 + 1≥N/2+1 个实例上 原子性 (SET NX PX) 地设置锁。
- 确保获取锁的总耗时 小于 锁的 TTL\text{TTL}TTL。
- 使用 全局唯一随机值 作为锁的凭证。
- 使用 Lua\text{Lua}Lua 脚本 保证释放锁的 原子性 和 安全性 (防止误释放)。
虽然 Redlock 在学术界存在一些争议(例如 Martin\text{Martin}Martin Fowler\text{Fowler}Fowler 的文章曾讨论过它的局限性,特别是在时钟同步和 Fencing\text{Fencing}Fencing Token\text{Token}Token 方面),但它无疑是目前 Redis\text{Redis}Redis 官方推荐的、易于理解和实现、且在多数场景下安全级别足够高的分布式锁方案。