如何用Redis实现分布式锁?
9.5.1 为什么Redis能用来实现分布式锁?
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:
Redis可以被多个客户端共享访问,可以用来保存分布式锁,且Redis的读写性能高,可以应对高并发的锁操作常见。
9.5.2 如何实现分布式锁?
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件:
-
加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们**使用 SET 命令带上 NX 选项**来实现加锁;
-
锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
-
锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;
SET lock_key unique_value NX PX 10000 # lock_key 就是 key 键; # unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作; # NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作; # PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端,即先判断先判断锁的 unique_value 是否为加锁客户端。因此,解锁是两个步骤,无法保证原子性。因此Redis需要引入Lua
脚本来实现原子操作,解锁的Lua
脚本如下:
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
用户通常需要编写适当的 Lua 脚本并通过 Redis 客户端发送到 Redis 服务器执行。这是因为分布式锁的解除需要原子操作,而 Lua 脚本可以保证在 Redis 中的操作是原子的。发送到Redis服务器方法如下:
## 后面的1是指有一个键
> EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_key unique_value
不过,为了简化这一过程,一些 Redis 客户端库已经内置了处理分布式锁的功能。这些库通常会封装 Lua 脚本的编写和执行过程,使用户可以更方便地使用分布式锁。
9.5.3 基于Redis的分布式锁的优缺点?
优点:
- 性能高效;
- 实现方便;
- 避免单点故障;
缺点:
- 超时时间不好设置;
- Redis 主从复制模式中的数据是异步复制的,这样**导致分布式锁的不可靠性**。
9.5.4 Redis如何解决集群情况下分布式锁的可靠性?
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
可以看到,加锁成功要同时满足两个条件:
- 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
- 条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。
资料参考
内容大多参考自:图解Redis介绍 | 小林coding (xiaolincoding.com)