Redis 分布式锁

本文将介绍分布式锁的概念以及用 Redis 实现分布式锁的做法。

一、单机锁和分布式锁

1. 简单单机锁

单机上的锁可以用一个变量表示:

  • 变量值为 0,表示没有线程获取锁
  • 变量值为 1,表示已经有线程获取到锁

获取锁和释放锁操作:

  • 当有线程调用获取锁操作时,检查变量值,当变量为 0 时,将变量设为 1,表示获取到锁,当变量为 1 时,返回错误信息,表示获取锁失败
  • 当有线程调用释放锁操作时,将变量值设为 0,以便其它线程获取锁

当然,这个单机锁还存在许多问题,例如:

  • 如果某个线程执行异常,无法主动释放锁,进而影响其它线程获取锁
  • 如果某个线程的锁可以被其它线程释放,将导致多个线程同时获取到锁

2. 分布式锁

与单机锁相同,分布式锁同样可以用一个变量表示,对锁的操作即是对变量的操作,不同的是,在分布式场景下,锁需要由一个共享存储系统维护,以便不同的服务可以访问到同一个锁。

3. 分布式锁的要求

  • 获取锁和释放锁操作涉及到多个子操作,应该保证操作的原子性
  • 共享存储系统负责维护锁,应该避免其故障或宕机,保证高可用

二、单 Redis 分布式锁

1. 简单实现

  • 锁用 Redis 的键值对表示

  • 使用 SETNX 命令获取锁,如下:

    1
    SETNX 锁名 1

    这个语句的作用是:

    当键值对存在时,什么都不做;

    当键值对不存在时,创建键值对并设值为 1

  • 使用 DEL 命令释放锁,如下:

    1
    DEL 锁名

2. 避免执行异常导致锁无法释放

可以给键值对设置过期时间,使锁超时后自动过期,如下:

1
SET 锁名 1 [EX seconds | PX milliseconds] NX

3. 保证锁只能被持有者释放

(1) 做法

可以在获取锁时将值设置为客户端的唯一标识,在释放锁使判断变量值,符合才允许释放。

(2) 获取锁

获取锁的代码如下:

1
SET 锁名 唯一标识 [EX seconds | PX milliseconds] NX

(3) 释放锁

释放锁时需要额外进行判断,实际上可以分为三步:

  • 获取锁值
  • 判断锁值是否与客户端的唯一标识相同
  • 相同时,释放锁

释放锁的操作应该是原子的(要么成功,要么失败,中间不能有其它命令执行),在 Redis 中,保证操作原子性的方式有:

  • 使用 Redis 的单命令操作

    例如 SETNX,它将判断和设置合为一个操作

  • 将多个操作写到 Lua 脚本中,执行 Lua 脚本以执行操作

由于没有合适的单命令操作,因此使用 Lua 脚本的方式保证操作原子性。

Lua 脚本如下:

1
2
3
4
5
6
// unlock.script 释放锁操作脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

释放锁操作如下:

1
redis-cli --eval unlock.script 锁名, 唯一标识

三、不可用的多 Redis 分布式锁

如果希望保证 Redis 高可用,我们一般会搭建集群。然而,假如在 Redis 主从未完全同步时发生主从切换,则上述的 Redis 分布式锁实现将会出问题。

考虑一下场景:

  • 客户端 A 获取了锁(状态 1)
  • 客户端 A 释放锁(状态 2)
  • 客户端 B 获取锁(状态 3)
  • Master 宕机
  • Slave 升级为新 Master,但未完全同步,停留在状态 1 / 状态 2

此时,Redis 集群中的锁状态是错误的。

四、基于 WAIT 命令的多 Redis 分布式锁

Redis 提供了 WAIT 命令,该命令会堵塞客户端,直到主库状态已经同步到子库。

1
2
SET resource_1 random_value NX EX 5
WAIT 1 5000

需要注意的是:

  • WAIT 命令只会阻塞发送该命令的客户端,不会影响其它客户端
  • WAIT 命令只能用于等待同步,并不能保证同步一定成功

五、基于 RedLock 算法的多 Redis 分布式锁

为了解决 Redis 作分布式锁时保证高可用的问题,Redis 的开发者 Antirez 提出了 RedLock 算法。

RedLock 算法的基本思路是:客户端与多个独立的 Redis 实例依次获取锁,如果能和半数以上的实例成功获取锁,则认为客户端成功获取锁,否则认为获取锁失败。这样一来,只要超过半数以上的实例能够工作,就可以确保整个分布式锁能够工作,保证了分布式锁的高可用性。

具体来说:

  • 部署多个独立的 Redis 实例

    不组成主从模式,均为主库,至少 5 个

  • 客户端获取当前时间 T1

  • 客户端按顺序依次向所有的 Redis 实例执行获取锁操作

    设置远小于锁过期时间的请求超时时间,如果请求直到超时都没有成功,跳过,继续请求下一个 Redis 实例

  • 当客户端完成所有的获取锁操作,进行计算,当且仅当 从超过半数的 Redis 实例中获取到锁 && 当前时间 T2 - T1 < 锁过期时间 时,认为获取锁成功

  • 若获取锁失败,向全部 Redis 实例发送释放锁请求

参考