0%

Redis - 分布式锁简述

在单实例的服务中,可以通过加锁的方式处理并发业务,例如synchronized,ReentrantLock等等,但是在分布式环境中,则需要依赖第三方的服务来辅助实现分布式锁。

Redis由于其超高的性能,成为支持分布式锁的主流框架之一,网上已有比较成熟的文章分析其实现方式,本文也只是以作者自己的角度梳理一遍,并没有什么新鲜的知识点。

设计一个Redis的分布式锁,至少需要满足如下两个特点:

  • 互斥性:同时只能有一个客户端能获取锁;
  • 锁的正确释放:锁要么到期自动释放,要么由获取锁的客户端释放,不能以其他方式被释放;

2.6.12版本以前的加锁

Redis的SETNX key value命令,能够天然的提供原子性的获取锁的功能,如果key已经存在,则表示锁已经被占用,则返回失败;否则设置成功,表示锁获取成功。

同时为了防止程序删除锁之前就崩溃,导致锁一直存在的问题,因此还需要使用EXPIRE key time来设置过期时间,组合起来即是:

1
2
SETNX key value
EXPIRE key expireTime

但是这种方式有个缺陷,如果在步骤一执行完毕之后,程序崩了,那么key还是没有被设置上过期时间,导致一直存在。

解决这个问题的方式在于,将存储的value结构化,一个大概的数据结构为:

1
2
3
4
{
"id":"client UUID",
"expire":"expire timestamp"
}

而获取锁的方式也更为复杂,不能简单的通过setnx命令来判断,而是当SETNX失败之后,再获取value并解析,如果通过EXPIRE字段发现锁已经过期,则表示锁未被成功释放,例如:

1
2
3
4
5
6
7
8
9
10
// ...
// setnx失败

value1 = GET key;
if value1.expire < now:
oldValue = GETSET key value2
if oldValue != value1:
return false;
else:
return true;

假设同时有多个客户端进入第6行逻辑,但是最终只能有一个客户端能获取锁成功,因此这里需要使用GETSET命令,对比GETSET返回的oldValue,只有与第4行获取的value1相同的客户端,才算是获取成功。

但是上述伪代码,仍然有问题,假设有多个客户端都进入了第6行,虽然最终只有一个客户端返回true,但是毕竟是两个客户端都执行了GETSET,因此没有获取到锁的那个客户端,使用value覆盖掉了第一个获取了锁的客户端设置的value。这个问题也有解决办法,这里不是本文的重点,就不再一直深究下去了。

2.6.12版本以后的加锁

可以看到2.6.12之前的实现方式,异常繁琐,在2.6.12版本之后,Redis丰富了SET命令,提供了如下的参数:

1
SET key value [EX seconds] [PX milliseconds] [NX|XX]

其中:

  • value:通常存储一个唯一ID,在解锁的时候保证是由同一个客户端加锁解锁;

  • EX/PX:表示设置键的过期时间,只是单位不同;

  • NX:键不存在时,才对键进行设置操作,若键已经存在,则返回失败;
  • XX:键存在时,才对键进行设置操作,若键不存在,则返回失败。

于是可以直接使用一个命令,解决锁的获取问题。

解锁

解锁逻辑,同样有个不容易被发现的问题,假设直接通过DEL命令去删除key,则如下场景下,会出现问题:

  • key设置的过期时间为10秒;
  • 程序获取锁之后,执行了15秒;
  • 程序通过DEL去删除key。

最后一步删除key,实际上有可能删除的是别的应用程序设置的key,因此在上面获取锁的时候,才要求value传入一个唯一ID,在删除的时候进行判断:

1
2
3
value = GET key;
if value.id == CLIENT_ID:
DEL key;

上述代码由于不是原子性的,因此依然可能会在两步之间发生其他问题,比如程序崩溃等等,因此最好的方式是:

1
2
script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
EVAL script [key] [CLIENT_ID];

第一行为一个简单的LUA脚本定义,第二行使用EVAL命令,将参数带入,执行脚本,其中:

  • KEYS[1] = key;
  • ARGV[1] = CLIENT_ID。

LUA脚本的含义是,如果获取的key的值与CLIENT_ID相等,则执行DEL命令;否则返回失败。

RedLock

上面单实例的Redis的实现方式,已经能够支持业务需求,但是Redis的作者antirez还提出了一种更高级的分布式锁的方案。

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。当且仅当从大多数(N/2+1)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。

集群中的分布式锁,主要问题是如何保障各个实例中的锁的失效时间一致的问题,文末的两篇文章中均有详细描述,可供参考。

参考



-=全文完=-