分布式锁是由共享存储系统维护的变量,多个客户端可以向共享存储系统发送命令进行加锁或释放锁操作。Redis 作为一个共享存储系统,可以用来实现分布式锁。
1. 分布式锁
通过redis命令来实现加锁
SET lock_key client_unique_value NX PX 10000
使用了NX选项,SET命令只有在键值对不存在的时候,才会设置,否则不做赋值操作。EX用来设置过期时间,单位秒,PX单位为毫秒
- 加锁涉及读取锁变量、检查锁变量和设置锁变量三个操作,需要保证原子性,首先,redis是单线程,所以不会并发执行命令;其次,通过SET NX 以及过期时间,一个命令包含三个操作,即可完成加锁操作。
- 需要加过期时间,是避免持有锁的客户端发生异常,无法主动释放锁,如果不释放锁,后续都无法加锁了。
- 设置的值需要区分不同客户端,避免锁被其他客户端释放。
通过redis lua脚本来实现解锁
//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
- 释放锁,涉及读取锁变量、判断值、删除锁变量多个操作。使用lua脚本,是为了保证释放锁操作的原子性。
2. RedLock
一个Redis实例保存锁变量,如果这个Redis实例发生宕机,锁变量就没了。客户端也无法进行锁操作,影响业务正常执行。为了保证分布式锁的可靠性,Redis官方提供了分布式锁算法RedLock。需要以下步骤完成加锁操作:
- 客户端获取当前时间。
- 客户端按顺序依次向N个Redis实例执行加锁操作。加锁操作和单实例上的加锁操作一致,为了保障某个Redis实例发生故障,RedLock算法还能继续运行,需要给加锁操作设置一个超时时间,这个超时时间远小于锁的有效时间。
- 一旦客户端完成了和所有Redis实例的加锁操作,客户端就要计算整个加锁过程的总耗时。
客户端只有在满足两个条件下,才认为加锁成功:
- 客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到锁。
- 客户端获取锁的总耗时没有超过锁的有效时间。
满足上述两个条件后,再重新计算这个锁的有效时间:锁的最初有效时间,减去客户端获取锁的总耗时。如果不满足上述两个条件,客户端要向所有Redis节点发起释放锁的操作。释放锁和单实例同样操作即可。
如果想要提升分布式锁的可靠性,可以通过RedLock算法来实现。不过也会增加成本,和复杂性。
RedLock算法有一些争论,Martin martin.kleppmann.com/2016/02/08/… antirez.com/news/101
3. Redisson分布式锁
主要研究RLock实现
- 利用hash结构存储uuid:threadId(field)和数量(value),这样实现可重入。
- 对于锁如果没有设置过期时间,将通过定时续锁的有效时长,来避免实例崩溃,但是后续无法加锁问题。
- Redisson加锁的api还提供等待锁释放的时间,所以当锁释放的是,还需要通过publish,把释放锁的消息给广播出去。
3.1 加锁
redis.call('exists', KEYS[1]) == 0
表示key不存在,redis.call('hexists', KEYS[1], ARGV[2]) == 1
表示同一个客户端在进行加锁。
-- KEYS[1] == key
-- ARGV[1] == expire time
-- ARGV[2] == uuid:threadId
if ((redis.call('exists', KEYS[1]) == 0) or (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]);
3.2 解锁
整体还是容易理解的,不在详细解释。
-- KEYS[1] == key
-- KEYS[2] == publish channel name
-- ARGV[1] == 0
-- ARGV[2] == 30000(LockWatchDog Timeout)
-- ARGV[3] == uuid:threadId
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0)
then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
4. 参考资料
- 《如何使用Redis实现分布式锁?》-- 极客时间
- mp.weixin.qq.com/s/2P2-ujcde…
- RedLock算法争论:zhangtielei.com/posts/blog-…
- Redisson源码