Redis分布式锁(1):实现原理

Posted by keys961 on September 8, 2020

1. 简介

在开发公司自研的分布式K-V缓存时,用到了ZooKeeper的选主算法,该算法本质上还是一个分布式锁的应用。算法大致如下:

  • 上锁

    1. lock-path下创建临时顺序节点n
    2. 获取lock-path下所有的节点,获取锁队列
    3. 判断创建的临时节点n是否拥有最小的序列号
      • 是:则获取锁
      • 否:监听小于n序列号的但序列号最大的节点p,监听移除事件,若监听到事件,则回到2尝试再获取锁

    获取到锁的就成为Leader

  • 解锁:直接删除临时节点

而后面我了解到Redis也有这样的功能,但由于我对于Redis的了解,尤其在开启复制的情况下,该锁理论上并不可靠(因为Redis弱一致),不过Redis的写毕竟还是比ZooKeeper快,这也是一大优势。

这里主要参考了https://redis.io/topics/distlock关于Redis分布式锁的说明,之后会看Redission对于Redis分布式锁的实现。

2. 保证

  1. Safety:需要满足互斥条件
  2. Liveness:
    • 不会出现死锁,客户端最后一定能获取到锁
    • 容错,只要大部分节点存活,客户端就能获取和释放锁

3. 简单方法SETNX及其弊端

一种简单的方式:

  • 客户端使用SETNX命令创建一个键值对,并设置过期时间,创建成功则获取到锁;使用DEL命令删除键值对以释放锁
  • 服务端使用主从复制容错

但由于Redis主从复制是异步的,因此当主节点宕掉,从节点不一定与主节点保证状态一致,可能导致

  1. 多个客户端同时获取一把锁,不满足互斥条件;

  2. 锁被错误的释放(A客户端释放了B客户端的锁);

  3. 此外,当锁过期时,若某个客户端卡顿,也会导致多个客户端同时获取一把锁,不满足互斥条件。

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) 上锁

  1. 串行向集群每个节点执行4.1.的命令,
    • 执行命令需要设置一个很短的超时时间,若命令没在超时时间内完成,则需要跳过向下一个节点发送4.1.的命令
  2. 客户端获取第2步执行命令的成功数量:
    • 若获取半数以上:获取到锁
    • 否则:没获取到锁,则对集群每个节点执行4.2.的命令以解锁

b) 解锁

解锁很简单,对集群每个节点执行4.2.的命令即可。

5.2. 延迟重启

考虑一个问题:

  • 有R1~R5节点
  • A从R1~R3获取锁,之后R1挂了并丢失锁状态
  • R1重启,B从R1, R4, R5获取锁,不满足互斥要求

解决方案是延迟重启,因为上锁会设置一个过期时间,我们可以在其他节点记录过期时长,并在过期时长后重启节点。

5.3. 存在的问题

  1. Redlock依赖墙上时钟(底层调用的是gettimeofday),该时钟并不保证单调递增,会时钟偏移和时钟漂移,过期清理可能导致锁出现错误的行为;
  2. 依旧有第3节中提及的第3个问题。

关于第1个问题,这个是没法解决的,个人也非常忌讳在分布式系统设计中使用墙上时钟(我认为墙上时钟真的只有“墙上”这个用途,除了给人看外没有其他一点用)。

而关于第2个问题,这个是一个租约问题,可以通过客户端进行续租(例如Redisson的watchdog)进行缓解,但是由于第1个问题存在,锁行为依旧可能出错。

6. 总结

总体而言,Redis分布式锁的算法相对也比较简单,但是它基于的Redis有非常致命的弱点,导致其正确性饱受质疑:

  1. 副本弱一致
  2. 依赖墙上时钟

个人比较认同Martin的看法,这个Redlock并不是一个非常好的解决方案:

  • 若为了效率,而比较不在意正确性,单点的算法就已经足够,没必要上Redlock
  • 若为了正确性,Redis的弱点太致命,不如上ZooKeeper, Chubby或etcd