1. 简介
在开发公司自研的分布式K-V缓存时,用到了ZooKeeper的选主算法,该算法本质上还是一个分布式锁的应用。算法大致如下:
-
上锁
- 在
lock-path
下创建临时顺序节点n
- 获取
lock-path
下所有的节点,获取锁队列 - 判断创建的临时节点
n
是否拥有最小的序列号- 是:则获取锁
- 否:监听小于
n
序列号的但序列号最大的节点p
,监听移除事件,若监听到事件,则回到2尝试再获取锁
获取到锁的就成为Leader
- 在
-
解锁:直接删除临时节点
而后面我了解到Redis也有这样的功能,但由于我对于Redis的了解,尤其在开启复制的情况下,该锁理论上并不可靠(因为Redis弱一致),不过Redis的写毕竟还是比ZooKeeper快,这也是一大优势。
这里主要参考了https://redis.io/topics/distlock关于Redis分布式锁的说明,之后会看Redission对于Redis分布式锁的实现。
2. 保证
- Safety:需要满足互斥条件
- Liveness:
- 不会出现死锁,客户端最后一定能获取到锁
- 容错,只要大部分节点存活,客户端就能获取和释放锁
3. 简单方法SETNX
及其弊端
一种简单的方式:
- 客户端使用
SETNX
命令创建一个键值对,并设置过期时间,创建成功则获取到锁;使用DEL
命令删除键值对以释放锁 - 服务端使用主从复制容错
但由于Redis主从复制是异步的,因此当主节点宕掉,从节点不一定与主节点保证状态一致,可能导致
-
多个客户端同时获取一把锁,不满足互斥条件;
-
锁被错误的释放(A客户端释放了B客户端的锁);
-
此外,当锁过期时,若某个客户端卡顿,也会导致多个客户端同时获取一把锁,不满足互斥条件。
4. 单点Redis解决方案
单点下,解决方案在3的基础上有小修改。
4.1. 获取锁
依旧使用SET lock_name identifier NX PX expire
命令。这里增加了identifier
作为标记,表示哪个客户端获取到了锁,这可以用于安全的释放锁。
4.2. 释放锁
释放时,需要校验identifier
,这里可以使用Lua脚本。它解决了第3节提及的第2个问题。
第3节提及的第2个问题例子:A获取了锁,但任务执行时间过长导致锁过期而释放,而之后B得到了锁,此时A任务执行完毕并显式释放锁,若不校验则会把B获取的锁错误释放。```
1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
但总体而言,单点的方案并没有解决第3节提及的第1个问题。
如A获取了锁,此时主节点宕机,锁状态没同步到从节点,B就可和A同时获取锁;
同样A释放锁,此时主节点宕机,锁状态没同步到从节点,B需要等待锁过期后才能获取锁。
同样,单点的方案也没有解决第3节提及的第3个问题。
5. 集群解决方案:Redlock
Redis Cluster实际上只做了数据分片,而每个分片实际可以视作一个单点,近似地,它和其他节点没有任何关系,所以集群解决方案下的分布式锁可以基于第4节的方案。
而第4节的方案遗留的问题1,本质上还是一个单点问题(即使往细讲原因是数据不一致),集群方案可以将锁状态维护在不同的分片上,即使某个分片故障,状态依旧得到保留,从而解决第3节的问题1。
5.1. 方案流程
a) 上锁
- 串行向集群每个节点执行4.1.的命令,
- 执行命令需要设置一个很短的超时时间,若命令没在超时时间内完成,则需要跳过向下一个节点发送4.1.的命令
- 客户端获取第2步执行命令的成功数量:
- 若获取半数以上:获取到锁
- 否则:没获取到锁,则对集群每个节点执行4.2.的命令以解锁
b) 解锁
解锁很简单,对集群每个节点执行4.2.的命令即可。
5.2. 延迟重启
考虑一个问题:
- 有R1~R5节点
- A从R1~R3获取锁,之后R1挂了并丢失锁状态
- R1重启,B从R1, R4, R5获取锁,不满足互斥要求
解决方案是延迟重启,因为上锁会设置一个过期时间,我们可以在其他节点记录过期时长,并在过期时长后重启节点。
5.3. 存在的问题
- Redlock依赖墙上时钟(底层调用的是
gettimeofday
),该时钟并不保证单调递增,会时钟偏移和时钟漂移,过期清理可能导致锁出现错误的行为; - 依旧有第3节中提及的第3个问题。
关于第1个问题,这个是没法解决的,个人也非常忌讳在分布式系统设计中使用墙上时钟(我认为墙上时钟真的只有“墙上”这个用途,除了给人看外没有其他一点用)。
而关于第2个问题,这个是一个租约问题,可以通过客户端进行续租(例如Redisson的watchdog)进行缓解,但是由于第1个问题存在,锁行为依旧可能出错。
6. 总结
总体而言,Redis分布式锁的算法相对也比较简单,但是它基于的Redis有非常致命的弱点,导致其正确性饱受质疑:
- 副本弱一致
- 依赖墙上时钟
个人比较认同Martin的看法,这个Redlock并不是一个非常好的解决方案:
- 若为了效率,而比较不在意正确性,单点的算法就已经足够,没必要上Redlock
- 若为了正确性,Redis的弱点太致命,不如上ZooKeeper, Chubby或etcd