写在前面
本文一起看下redis作为分布式锁使用的相关内容。
1:怎么算是锁或没锁
锁和没锁本身其实就是用一个变量的值来表示,比如变量lock,当值为1时代表处于上锁状态,当值为0时表示没有锁,那么多线程想要获取锁的话就是判断当前lock值是否为0,如果是的话则将其设置为1,则代表该线程获取到了锁,释放锁的过程就是将lock的设置为0。当然,基本原理就是这样,实际中可能略有不同。伪代码可能如下:
接下来我们分别从基于单实例redis和多实例redis两种情况来看下如何加锁。
1.1:单实例redis
我们前面分析了,加锁其实就是对一个变量赋予特定值,成功则是加锁成功,否则则是加锁失败,还有就是,这个过程必须是原子的,我们也知道,redis中的原子操作有两种,第一种是单命令,第二种是lua脚本,我们先看第一种,redis提供了命令setnx ,该命令的行为是如果是key不存在,则设置,否则什么也不做,如下:
127.0.0.1:6379> setnx aaaaa bbbb
(integer) 1
127.0.0.1:6379> setnx aaaaa bbbb
(integer) 0
可以看到再次调用就返回0了。因此可以使用其来实现加锁的操作,其实这样还是不够的,因为缺少释放锁的操作,释放也很简单,只需要执行del 但是此时可能存在如下2个问题:
1:如果最终del没有执行成功,则锁将会一直不能释放
2:执行del的线程不一定是加锁的线程,如线程A加锁了,但是线程B执行了del,但此时线程A还没有执行完毕,线程C又拿到了锁,就会出现问题了
对于1
,可以通过expire 命令来设置过期时间,对于2,我们可以给每个客户端分配特有的值,进行设置,即setnx key val中的val每个客户端都是不一样的,在进行删除的时候,判断当前是否为自己加的锁,是才del,否则不操作,为了保证原子性,我们需要使用lua脚本来实现这个锁的过程:
// local key = KEYS[1]
// local val = redis.call("GET", key);
// 其中ARGV[1]代表的是客户端设置的值,只有当前值是自己设置的时候才执行删除key操作
if redis.call("GET", KEYS[1]) == ARGV[1]
then
redis.call('del', KEYS[1])
return 1
else
return 0
end
其实,以上对于问题1,还需要单独再执行一个命令expire,这显然会影响到程序的性能,对于这点,我们可以使用set命令来完成,不存在才设置值以及同时设置过期时间,该命令格式如下:
set key value [EX seconds] [PX milliseconds] [NX|XX]
其中NX用来设置当key不存在时才设置,PX用来设置过期的时长,此时加锁代码可能如下:
127.0.0.1:6379> set lock:key client_val NX PX 10000
OK
即10秒后过期并删除。解锁lua脚本可能如下:
local key = KEYS[1]
local val = redis.call("GET", key);
// 其中ARGV[1]代表的是客户端设置的值,只有当前值是自己设置的时候才执行删除key操作
if val == ARGV[1]
then
redis.call('del', KEYS[1])
return 1
else
return 0
end
其实和使用setnx命令的解锁方式是一样的。单实例锁的问题是,一旦实例宕机,就没有办法加锁和解锁了,应用程序也就无法正常运行了。
1.2:多实例redis锁
因为是多个实例同时操作,所以加锁过程一定要遵循一定的规范和过程,redlock分布式锁算法就描述了这样的一个规范和过程,该算法如下:
1:记录当前系统的时间
2:使用set k v NX PX millisec 完成加锁
3:所有实例加锁完成后,按照如下条件判断是否加锁成功
3.1:是否超过半数的实例加锁成功
3.2:当前时间减去第一步记录的加锁开始时间是否超过了锁的超时时间,没超过则成功
释放锁的过程和单实例类似,只不过需要每个实例上都执行释放锁的lua脚本。
写在后面
参考文章列表:
Redlock:Redis分布式锁最牛逼的实现 。