Redis实现分布式锁的一些问题

371 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. setnx的作用

setnx命令返回整数值,当返回1时表示设置值成果,当返回0时表示设置值失败(key已存在)。

一般不直接使用setnx命令来实现分布式锁,因为为了避免出现死锁,要给锁设置一个自动过期时间。而setnx命令和设置过期时间的命令不是原子的,可能加锁成果而设置过期时间失败,依然存在死锁的隐患。

对于这种情况,Redis改进了set命令,给它增加了nx选项,启用该选项时set命令的效果就会setnx一样了。

2. 使用Redis的set..nx..命令实现分布式锁

采用Redis实现分布式锁,就是在Redis里存一份代表锁的数据,通常用字符串即可。采用改进后的setnx命令(即set...nx...命令)实现分布式锁的思路,以及优化的过程如下:

加锁:

第一版,这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。

setnx key value

第二版,给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。

setnx key value expire key seconds

第三版,通过“set...nx...”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。

set key value nx ex seconds 

解锁:

解锁就是删除代表锁的那份数据。

del key

此时的问题:

此时的隐患如下图。进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,它此时释放的可能是其他线程的锁。

image.png

3. set..nx..+Lua脚本的方式实现分布式锁

在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key赋一个随机值,来充当进程的标识。

解锁时要先判断、再释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。

# 加锁 
set key random-value nx ex seconds   
# 解锁 
if redis.call("get",KEYS[1]) == ARGV[1]
    then
    return redis.call("del",KEYS[1]) 
else     
    return 0 
end

4. 基于RedLock算法的分布式锁:

上述分布式锁的实现方案,是建立在单个主节点之上的。它的潜在问题如下图所示,如果进程A在主节点上加锁成功,然后这个主节点宕机了,则从节点将会晋升为主节点。若此时进程B在新的主节点上加锁成果,之后原主节点重启,成为了从节点,系统中将同时出现两把锁,这是违背锁的唯一性原则的。

image.png 总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的。若要保证分布式锁的高可用,则可以采用多个节点的实现方案。这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案。该算法基于多个Redis节点,它的基本逻辑如下:

  • 这些节点相互独立,不存在主从复制或者集群协调机制;
  • 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;
  • 解锁:向所有的实例发送DEL命令,进行解锁;

RedLock算法的示意图如下,也可以自己实现该算法,也可以直接使用Redisson框架。

image.png