Redlock:为什么你的 Redis 分布式锁需要不止一个节点?
1 啥是RedLock?
在Redis客户端实现的分布式锁算法,比单节点的方法更安全。
2 特性
2.1 安全
互斥访问,即永远只有一个 client 能拿到锁
2.2 避免死锁
最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
2.3 容错性
只要大部分 Redis 节点存活就可以正常提供服务
3 单节点实现分布式锁
3.1 加锁
SET resource_name my_random_value NX PX 30000
- 仅当 Key 不存在时(NX保证)set 值
- 且设置TTL 3000ms (PX保证)
- 值 my_random_value 须是所有 client 和所有锁请求发生期间唯一的
3.2 释放锁
-- 避免释放了另一client创建的锁
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
若只有del命令,则万一client1拿到lock1后,因故阻塞很久,此时Redis Server端的lock1已过期,并已被重新分配给client2,则client1此时再去释放这把锁就会造成client2原本获取到的锁被client1无故释放。因此,为每个client分配一个unique string值即可避免该问题。
4 Redlock 算法
起 5 个 master 节点,分布在不同机房,以尽量保证可用性。
4.1 获得锁
client的操作:
- 获取当前时间(单位ms)
- 轮流用相同的key和随机值在N个节点上请求锁。client在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如若锁的TTL=10s,则每个节点锁请求的超时时间可能是5-50ms,可防止一个client在某个已宕机的master节点阻塞过长时间,若一个master节点不可用,尽快尝试下一master节点
- client计算第②步中获取锁所费时间,仅当client在过半数的master节点成功获取锁,且总消耗时间不超过锁释放时间,这个锁就认为【获取成功】
获取结果,若锁获取:
- 成功,则现在锁的TTL就是【最初的锁释放时间】- 【之前获取锁所消耗的时间】
- 失败,不管是如下啥原因,client都会到每个master节点上释放锁,即便是那些他认为没获取成功的锁:
- 获取成功的锁没超一半(N/2+1)
- 总消耗时间超过锁的TTL
4.2 失败重试
若一个client申请锁失败,则它需稍等一会再重试,避免多个client同时申请锁。最好就是一个client需要几乎同时向 5 个 master 发起锁申请。
若client申请锁失败,它需尽快在曾申请到锁的 master 执行 unlock 操作,便于其他client获得该锁,避免这些锁过期造成的时间浪费。若此时网络分区使得client无法联系这些master,那这种浪费就是不得不付出的代价。
4.3 释放锁
依次释放所有节点上的锁。
性能、崩溃恢复和 fsync
很多使用Redis做锁服务器的用户在获取锁和释放锁时不止要求低延时,同时要求高吞吐量,即单位时间内可以获取和释放的锁数量。
为达到这个要求,一定会使用多路传输来和N个服务器进行通信以降低延时(或者也可以用假多路传输,也就是把socket设置成非阻塞模式,发送所有命令,然后再去读取返回的命令,假设客户端和不同Redis服务节点的网络往返延时相差不大的话)。
想让系统可以自动故障恢复的话,还需要考虑一下信息持久化。
假设我们Redis都是配置成非持久化的,某个客户端拿到了总共5个节点中的3个锁,这三个已经获取到锁的节点中随后重启了,这样一来我们又有3个节点可以获取锁了(重启的那个加上另外两个),这样一来其他客户端又可以获得这个锁了,这样就违反了我们之前说的锁互斥原则。
如果我们启用AOF持久化功能,情况会好很多。举例来说,我们可以发送SHUTDOWN命令来升级一个Redis服务器然后重启之,因为Redis超时时效是语义层面实现的,所以在服务器关掉期间时超时时间还是算在内的,我们所有要求还是满足了的。然后这个是基于我们做的是一次正常的shutdown,但是如果是断电这种意外停机呢?
如果Redis是默认地配置成每秒在磁盘上执行一次fsync同步文件到磁盘操作,那就可能在一次重启后我们锁的key就丢失了。理论上如果我们想要在所有服务重启的情况下都确保锁的安全性,我们需要在持久化设置里设置成永远执行fsync操作,但是这个反过来又会造成性能远不如其他同级别的传统用来实现分布式锁的系统。 然后问题其实并不像我们第一眼看起来那么糟糕,基本上只要一个服务节点在宕机重启后不去参与现在所有仍在使用的锁,这样正在使用的锁集合在这个服务节点重启时,算法的安全性就可以维持,因为这样就可以保证正在使用的锁都被所有没重启的节点持有。
为了满足这个条件,我们只要让一个宕机重启后的实例,至少在我们使用的最大TTL时间内处于不可用状态,超过这个时间之后,所有在这期间活跃的锁都会自动释放掉。 使用延时重启的策略基本上可以在不适用任何Redis持久化特性情况下保证安全性,然后要注意这个也必然会影响到系统的可用性。举个例子,如果系统里大多数节点都宕机了,那在TTL时间内整个系统都处于全局不可用状态(全局不可用的意思就是在获取不到任何锁)。