一、基于单个redis节点的分布式锁
步骤1:
向redis发送命令,获取锁
SET resource_name my_random_value NX PX 30000
解释说明:
my_random_value
:
客户端生成的
随机
值
,要保证在足够长的时间内所有客户端生成的随机值是
唯一的
NX:
当key为resource_name的值不存在时,才能被成功插入(
IF NOT EXISTS
)。这一点保证了只有一个客户端能设置成功,换句话说只有一个客户端能拿到锁。
PX:
表示过期时间为30s
步骤2:
获取锁成功后才能访问共享资源
步骤3:释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
备注:KEYS[1]为resource_name,ARGV[1]:为my_random_value
二、重点问题分析
1、必须设置过期时间:
防止客户端崩溃或者跟redis通信中断时,导致无法释放锁的问题出现
2、要保持操作的原子性
网络上出现很多介绍redis锁是用以下方式:
1、SETNX resource_name my_random_value
2、EXPIRE resource_name 30
虽然这两个命令跟前面set nx px的效果一样,但是它将
设置key+设置过期时间的操作分开
,不能保证获取锁操作的原子性。
- 当客户端执行完setnx后崩溃,那么它就没有机会释放锁
- 当客户端执行完setnx成功且expire了过期时间,但可能在它访问共享资源时锁过期了,这时可能已经有另外一个客户端也在访问同一个共享资源。
3、客户端生成的
my_random_value随机值是唯一的,
这样才能保证在释放锁时删除的key是自己生成的那一个(不会出现误删)
假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:
- 客户端1获取锁成功。
- 客户端1在某个操作上阻塞了很长时间。
- 过期时间到了,锁自动释放了。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。
之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。
4、释放锁的操作必须使用Lua脚本来实现(为了保证删除操作的原子性)
释放锁其实包含三步操作:
'GET'、判断和'DEL'
,用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列:
- 客户端1获取锁成功。
- 客户端1访问共享资源。
- 客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。
- 客户端1判断随机字符串的值,与预期的值相等。
- 客户端1由于某个原因阻塞住了很长时间。
- 过期时间到了,锁自动释放了。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。
实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了
大的网络延迟
,也有可能导致类似的执行序列发生。
三、基于单redis节点无法解决的问题
#若redis没有配置高可用,当redis唯一节点宕机后,那么所有客户端就无法获得锁了,锁机制失效。因此为了提高redis的高可用,设置了主从节点。
也因此产生了另外一个新问题。
#问题描述:
我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但
由于Redis的主从复制(replication)是
异步的
(即主从复制存在一定的时间差,很容易在这个时间差里打擦边球)
,这可能导致在failover过程中丧失锁的安全性。
考虑下面的执行序列:
- 客户端1从Master获取了锁。
- Master宕机了,存储锁的key还没有来得及同步到Slave上。
- Slave升级为Master。
- 客户端2从新的Master获取到了对应同一个资源的锁。
于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了Redlock算法
其它疑问
1、px time设置成多少合适呢?
设置少了可能会导致客户端访问共享资源之前[锁过期],设置太长了呢,可能会导致客户端释放锁失败时,导致其它客户端的长时间等待。
2、可能因为长时间的阻塞(网络延迟),导致客户端获得的锁过期,而使得共享资源失去了保护,那么有没有什么方案来解决这些存在的疑问呢?
请看下一篇文章[
redis分布式锁真的安全吗?(二):
分布式锁Redlock]