Redis [5]

分布式锁

Posted by ZYT on November 17, 2018

分布式锁设计原则:

  • 互斥性,同一时间只有一个线程持有锁
  • 容错性,即使某一个持有锁的线程,异常退出,其他线程仍可获得锁
  • 隔离性,线程只能解自己的锁,不能解其他线程的锁

1、基于单节点 Redis 的分布式锁

流程:

1. 获取锁

> set key-name random-value nx px 300
其中:
- random-value 是由客户端生成的一个随机字符串,需要保证在足够长的一段时间内的唯一性
- nx 只有 key-name 不存在时,执行成功
- px 过期时间,是锁的有效时间

2. 访问共享资源

3. 释放锁

----------- lua -----------
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

调用 lua 脚本,保证原子性

基于单节点 Redis 的分布式锁无法解决 failover 的问题:

假如 Redis 节点宕机了,那么所有客户端就都无法获得锁了,服务变得不可用。为了提高可用性,我们可以给这个Redis节点挂一个 Slave,当 Master 节点不可用的时候,系统自动切到Slave 上(failover)。但由于 Redis 的主从复制是异步的,这可能导致在 failover 过程中丧失锁的安全性。如下:

  1. 客户端 1 从 Master 获取了锁
  2. Master 宕机了,存储锁的 key 还没有来得及同步到 Slave 上
  3. Slave 升级为 Master
  4. 客户端 2 从新的 Master 获取到了对应同一个资源的锁

于是,客户端 1 和客户端 2 同时持有了同一个资源的锁,锁的安全性被打破。针对上述问题,Redis 的作者设计了 Redlock 算法。

上述算法还存在一个问题,锁的有效时间:

如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作

2、Redlock

Redlock 基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)

流程:

1. 获取锁

- 获取当前时间(毫秒数)
- 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,
  包含随机字符串 random-value,也包含过期时间。为了保证在某个Redis节点不可用的时候算法能够继续
  运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户
  端在向某个 Redis 节点获取锁失败以后,应该立即尝试下一个 Redis 节点
- 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第 1 步记录的时间。如果客户端从
  大多数 Redis 节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间
  (lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败
- 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第 3 步计算
  出来的获取锁消耗的时间
- 如果最终获取锁失败了(可能由于获取到锁的 Redis 节点个数少于 N/2+1,或者整个获取锁的过程消耗的
  时间超过了锁的最初有效时间),那么客户端应该立即向所有 Redis 节点发起释放锁的操作

2. 访问共享资源

3. 释放锁

客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否

如果有节点发生崩溃重启,还是会对锁的安全性有影响,过程如下:

  1. 客户端 1 成功锁住了 A ,B,C ,获取锁成功(但 D 和 E 没有锁住)
  2. 节点 C 崩溃重启了,但客户端 1 在 C 上加的锁没有持久化下来
  3. 节点 C 重启后,客户端 2 锁住了 C,D,E,获取锁成功

这样客户端 1 和 2 同时获得了针对同一资源的锁

针对节点重启引发的锁失效问题,Redis 作者又提出了延迟重启的概念,也就是一个节点崩溃后,先不立即重启它,而是等待一段时间这段时间应该大于锁的有效时间在重启。

对于 Redlock 曾有过很详细的争论,大家可以移步这两篇文章:

基于Redis的分布式锁到底安全吗(上)

基于Redis的分布式锁到底安全吗(下)