Redis 分布式锁
本文将介绍分布式锁的概念以及用 Redis 实现分布式锁的做法。
一、单机锁和分布式锁
1. 简单单机锁
单机上的锁可以用一个变量表示:
- 变量值为 0,表示没有线程获取锁
- 变量值为 1,表示已经有线程获取到锁
获取锁和释放锁操作:
- 当有线程调用获取锁操作时,检查变量值,当变量为 0 时,将变量设为 1,表示获取到锁,当变量为 1 时,返回错误信息,表示获取锁失败
- 当有线程调用释放锁操作时,将变量值设为 0,以便其它线程获取锁
当然,这个单机锁还存在许多问题,例如:
- 如果某个线程执行异常,无法主动释放锁,进而影响其它线程获取锁
- 如果某个线程的锁可以被其它线程释放,将导致多个线程同时获取到锁
2. 分布式锁
与单机锁相同,分布式锁同样可以用一个变量表示,对锁的操作即是对变量的操作,不同的是,在分布式场景下,锁需要由一个共享存储系统维护,以便不同的服务可以访问到同一个锁。
3. 分布式锁的要求
- 获取锁和释放锁操作涉及到多个子操作,应该保证操作的原子性
- 共享存储系统负责维护锁,应该避免其故障或宕机,保证高可用
二、单 Redis 分布式锁
1. 简单实现
锁用 Redis 的键值对表示
使用 SETNX 命令获取锁,如下:
1
SETNX 锁名 1
这个语句的作用是:
当键值对存在时,什么都不做;
当键值对不存在时,创建键值对并设值为 1
使用 DEL 命令释放锁,如下:
1
DEL 锁名
2. 避免执行异常导致锁无法释放
可以给键值对设置过期时间,使锁超时后自动过期,如下:
1 |
|
3. 保证锁只能被持有者释放
(1) 做法
可以在获取锁时将值设置为客户端的唯一标识,在释放锁使判断变量值,符合才允许释放。
(2) 获取锁
获取锁的代码如下:
1 |
|
(3) 释放锁
释放锁时需要额外进行判断,实际上可以分为三步:
- 获取锁值
- 判断锁值是否与客户端的唯一标识相同
- 相同时,释放锁
释放锁的操作应该是原子的(要么成功,要么失败,中间不能有其它命令执行),在 Redis 中,保证操作原子性的方式有:
使用 Redis 的单命令操作
例如 SETNX,它将判断和设置合为一个操作
将多个操作写到 Lua 脚本中,执行 Lua 脚本以执行操作
由于没有合适的单命令操作,因此使用 Lua 脚本的方式保证操作原子性。
Lua 脚本如下:
1 |
|
释放锁操作如下:
1 |
|
三、不可用的多 Redis 分布式锁
如果希望保证 Redis 高可用,我们一般会搭建集群。然而,假如在 Redis 主从未完全同步时发生主从切换,则上述的 Redis 分布式锁实现将会出问题。
考虑一下场景:
- 客户端 A 获取了锁(状态 1)
- 客户端 A 释放锁(状态 2)
- 客户端 B 获取锁(状态 3)
- Master 宕机
- Slave 升级为新 Master,但未完全同步,停留在状态 1 / 状态 2
此时,Redis 集群中的锁状态是错误的。
四、基于 WAIT 命令的多 Redis 分布式锁
Redis 提供了 WAIT
命令,该命令会堵塞客户端,直到主库状态已经同步到子库。
1 |
|
需要注意的是:
WAIT
命令只会阻塞发送该命令的客户端,不会影响其它客户端WAIT
命令只能用于等待同步,并不能保证同步一定成功
五、基于 RedLock 算法的多 Redis 分布式锁
为了解决 Redis 作分布式锁时保证高可用的问题,Redis 的开发者 Antirez 提出了 RedLock 算法。
RedLock 算法的基本思路是:客户端与多个独立的 Redis 实例依次获取锁,如果能和半数以上的实例成功获取锁,则认为客户端成功获取锁,否则认为获取锁失败。这样一来,只要超过半数以上的实例能够工作,就可以确保整个分布式锁能够工作,保证了分布式锁的高可用性。
具体来说:
部署多个独立的 Redis 实例
不组成主从模式,均为主库,至少 5 个
客户端获取当前时间 T1
客户端按顺序依次向所有的 Redis 实例执行获取锁操作
设置远小于锁过期时间的请求超时时间,如果请求直到超时都没有成功,跳过,继续请求下一个 Redis 实例
当客户端完成所有的获取锁操作,进行计算,当且仅当
从超过半数的 Redis 实例中获取到锁 && 当前时间 T2 - T1 < 锁过期时间
时,认为获取锁成功若获取锁失败,向全部 Redis 实例发送释放锁请求