使用Redis实现分布式锁

一.介绍

分布式锁,或者称为“全局锁”,在分布式环境中,保证锁只能被一个对象(或者成为“事务”)获取,经常出现在“避免数据重复处理”、“接口幂等”的场景。

下面介绍了Redis中两种分布式锁的实现方式。

二.setnx + expire组合

2.1命令介绍

使用setnx和expire命令组合实现,这两个命令用法如下:

setnx key value
expire key seconds

对于setnx来说,只有key不存在,或者已经过期,setnx才会成功(返回1),否则失败(返回0)。

expire可以用来对key的有效期进行设置,若不设置key的有效期,则默认为-1,表示一直有效;

2.2操作步骤

实现分布式锁的时候,方式很简单:

2.现有A、B两个线程尝试获取同一个全局锁,假设先接收到A的请求;

3.直接执行setnx命令,key为foo,value可以根据业务制定,比如A的名称或者某个特殊的值;

4.如果setnx加锁成功,那么使用expire去设置key的过期时间,防止一直不释放锁的情况出现;

5.加锁成功后,且设置过期时间成功后,执行自己的业务逻辑(获取到全局锁的逻辑),等待锁自动过期释放或者手动删除(del命令)

6.如果setnx加锁失败,则根据自己业务逻辑进行其他操作(未获取到全局锁的逻辑)。

2.3释放锁存在的问题

对于锁的释放,有两种方式选择:锁自动过期释放锁,手动删除锁。其中让锁过期自动释放比较简单,手动删除锁存在一些问题。

如果选择锁自动过期的方式来释放锁,那么需要注意过期时间不要设置太长,不然大量的请求会阻塞,导致系统效率降低;

如果选择手动删除,可以通过key对应的value来判断加锁和释放锁(del)的是否为同一个事务,如果是的话,则进行删除操作,但是这个不是原子操作,原因如下:

1.查询锁是否本次加的锁,如果是,则进行第2步,否则就是已经过期了,被其他线程获取了锁;

2.进行del操作;

在第一步的查,到第二步的删,是存在时间间隔的,这段时间内,锁可能会过期并被其他线程获取到,此时再del,则会让其他线程获取到的锁失效。

可以考虑使用watch命令来对key进行监听。

2.4设置过期时间存在的问题

如果在setnx之后,还没有来得及expire设置过期时间,那么锁就一直不会释放,后续请求无法再加锁。

此时可以使用带有多个参数的set命令:

set key value [EX seconds] [PX milliseconds] [NX|XX]

http://www.redis.cn/commands/set.html

2.5锁可重入的问题

可以通过value判断是否为本事务获取了事务,如果是,则直接进入。

三.Redis + Lua实现分布式锁

使用Redisson框架,下面简单说一下原理

3.1加锁的Lua伪代码

可以使用Lua编程语言,编写一段代码,然后在redis中执行,可以认为是原子操作,下面是伪代码:

if (redis.call(‘exists‘, keys[1]) == 0) then
	redis.call(‘hset‘, keys[1], argv[2], 1);
	redis.call(‘pexpire‘, keys[1], argv[1]);
	return nil;
end

if (redis.call(‘hexists‘, keys[1], argv[2]) == 1) then
	redis.call(‘hincrby‘, keys[1], argv[2], 1);
	redis.call(‘pexpire‘, keys[1], argv[1]);
	return nil;
end

return redis.call(‘pttl‘, keys[1]);

上面涉及一个redis.call()方法,他的功能根据传入的参数执行redis命令,

以及出现了几个参数,解释如下:

keys[1]:表示的就是锁的名称,比如要对foo这个字符串加锁,那么keys[1]就是foo;

argv就是argument value的简写,表示的是传入的参数列表,是一个数组;

argv[1]:表示锁的生存时间,默认在30秒后释放锁(失效);

argv[2]:表示加锁的客户端ID(可以理解为事务ID)

3.2加锁的代码介绍

分别介绍上面三段代码:

if (redis.call(‘exists‘, keys[1]) == 0) then
	redis.call(‘hset‘, keys[1], argv[2], 1);
	redis.call(‘pexpire‘, keys[1], argv[1]);
	return nil;
end

这段代码,判断key是否存在,如果不存在,则创建一个hash,key为锁的名称,值为客户端id,后面的1表示加锁的次数,后面在判断可重入的时候使用;然后设置key的有效时间;之后就返回锁获取成功。

if (redis.call(‘hexists‘, keys[1], argv[2]) == 1) then
	redis.call(‘hincrby‘, keys[1], argv[2], 1);
	redis.call(‘pexpire‘, keys[1], argv[1]);
	return nil;
end

这段代码主要实现“锁的可重入”,前提是key(锁)已经存在了,那么就判断hash中key对应的客户端是否为当前客户端id,如果是的话,那么就执行hincrby命令,将加锁的次数进行加1,然后重新设置key(锁)的过期时间,之后再返回。

return redis.call(‘pttl‘, keys[1]);

执行到上面这段代码,表示锁已经存在,且不是当前客户端获取到锁,那么就会执行pttl查看锁的有效期。

3.3watch dog自动延时

如果客户端获取到锁后,超过设置的过期时间,还希望持有锁,那么有watch dog机制,也就是该客户端获取到锁后,就会启动后台线程去判断客户端是否仍旧持有锁,如果是,则会延长过期时间。

3.4释放锁

释放锁,需要注意有锁的重入问题,当锁重入后,有计数器来保存重入的次数(获取锁的次数),每次unlock的时候,将计数减1,当计数为0的时候,表示不再持有该锁,则执行del key[1]命令删除锁。

相关推荐