前言
上篇文章介绍了通过
SET key_name my_random_value NX PX 30000
NX 表示if not exist 就设置并返回True,否则不设置并返回False
PX 表示过期时间用毫秒级, 30000 表示这些毫秒时间后此key过期
方式实现的redis分布锁
但有缺点:
只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
在Redis的master节点上拿到了锁;
但是这个加锁的key还没有同步到slave节点;
master故障,发生故障转移,slave节点升级为master节点;
导致锁丢失。
由此 redis官方推荐 redlock 来解决这个问题
使用场景
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
优点
-
防止了 单节点故障造成整个服务停止运行的情况;
-
在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法
概念说明
-
1.TTL:Time To Live;只 redis key 的过期时间或有效生存时间
-
2.clock drift:时钟漂移;指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值;如果电脑距离过远会造成时钟漂移值 过大
最低保证分布式锁的有效性及安全性的要求
-
1.互斥;任何时刻只能有一个client获取锁
-
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
-
3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁
使用redession实现分布锁的过程
假设有5个完全独立的redis主服务器
-
1.获取当前时间戳
-
2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
-
3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
-
4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
-
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
代码实现
maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.4.3</version>
</dependency>
获取redession客户端连接
业务处理接口
具体的业务逻辑实现都需要实现该接口
封装锁管理接口
锁管理接口实现类
调用加锁
redessoin获取锁和释放锁源码分析
获取锁
跟踪RLock类的lock.tryLock(100, lockTime, TimeUnit.SECONDS);方法
可以找到方法
现在来分析下其中的lua脚本
if (redis.call('exists', KEYS[1]) == 0)
首先分布式锁的KEY不能存在,,
then redis.call('hset', KEYS[1], ARGV[2], 1);
如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1)
redis.call('pexpire', KEYS[1], ARGV[1]);
并通过pexpire设置失效时间(也是锁的租约时间)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
那么重入次数加1,并且设置失效时间
return redis.call('pttl', KEYS[1])
获取分布式锁的KEY的失效时间毫秒数
释放锁
if (redis.call('exists', KEYS[1]) == 0)
then redis.call('publish', KEYS[2], ARGV[1])
如果分布式锁KEY不存在,那么向channel发布一条消息
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then return nil
如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
如果就是当前线程占有分布式锁,那么将重入次数减1
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]);
重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除
return 0; else redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息
DEMO源码
https://gitee.com/pingfanrenbiji/redis-demo
参考文章
https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
https://yq.aliyun.com/articles/674394
本文使用 mdnice 排版