【分布式锁】02-使用Redisson实现公平锁原理
前言
前面分析了Redisson可重入锁的原理,主要是通过lua脚本加锁及设置过期时间来保证锁执行的原子性,然后每个线程获取锁会将获取锁的次数+1,释放锁会将当前锁次数-1,如果为0则表示释放锁成功。
可重入原理和JDK中的可重入锁都是一致的。
Redisson公平锁原理
JDK中也有公平锁和非公平锁,所谓公平锁,就是保证客户端获取锁的顺序,跟他们请求获取锁的顺序,是一样的。公平锁需要排队,谁先申请获取这把锁,谁就可以先获取到这把锁,是按照请求的先后顺序来的。
Redisson实现公平锁源码分析
公平锁使用也很简单:
1RLock lock = redisson.getFairLock("anyLock");
2lock.lock();
3lock.unlock();
核心lua脚本代码:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
long currentTime = System.currentTimeMillis();
if (command == RedisCommands.EVAL_LONG) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// remove stale threads
"while true do "
+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
+ "if firstThreadId2 == false then "
+ "break;"
+ "end; "
+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
+ "if timeout <= tonumber(ARGV[4]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "
+ "else "
+ "break;"
+ "end; "
+ "end;"
+ "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
"redis.call('lpop', KEYS[2]); " +
"redis.call('zrem', KEYS[3], ARGV[2]); " +
"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; " +
"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
"local ttl; " +
"if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " +
"ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" +
"else "
+ "ttl = redis.call('pttl', KEYS[1]);" +
"end; " +
"local timeout = ttl + tonumber(ARGV[3]);" +
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end; " +
"return ttl;",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName),
internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime);
}
throw new IllegalArgumentException();
}
KEYS/ARGV参数分析
KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName)
- KEYS1 = getName() = 锁的名字,“anyLock”
- KEYS[2] = threadsQueueName = redisson_lock_queue:{anyLock},基于redis的数据结构实现的一个队列
- KEYS[3] = timeoutSetName = redisson_lock_timeout:{anyLock},基于redis的数据结构实现的一个Set数据集合,有序集合,可以自动按照你给每个数据指定的一个分数(score)来进行排序
ARGV = internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime,
currentTime
- ARGV1 = 30000毫秒
- ARGV[2] = UUID:threadId
- ARGV[3] = 当前时间(10:00:00) + 5000毫秒 = 10:00:05
- ARGV[4] = 当前时间(10:00:00)
模拟不同线程获取锁步骤
- 客户端A thread01 10:00:00 获取锁(第一次加锁)
- 客户端B thread02 10:00:10 获取锁
- 客户端C therad03 10:00:15 获取锁
lua脚本源码分析
客户端A thread01 加锁分析
thread01 在10:00:00 执行加锁逻辑,下面开始一点点分析lua脚本执行代码:
1"while true do "
2+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
3+ "if firstThreadId2 == false then "
4 + "break;"
lindex redisson_lock_queue:{anyLock} 0,就是从redisson_lock_queue:{anyLock}这个队列中弹出来第一个元素,刚开始,队列是空的,所以什么都获取不到,此时就会直接退出while true死循环
1"if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
2+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
3"redis.call('lpop', KEYS[2]); " +
4"redis.call('zrem', KEYS[3], ARGV[2]); " +
5"redis.call('hset', KEYS[1], ARGV[2], 1); " +
6"redis.call('pexpire', KEYS[1], ARGV[1]); " +
7"return nil; " +
8"end; " +
这段代码判断逻辑的意思是:
- exists anyLock,锁不存在,也就是没人加锁,刚开始确实是没人加锁的,这个条件肯定是成立的;
- 或者是exists redisson_lock_queue:{anyLock},这个队列不存在
- 或者是lindex
redisson_lock_queue:{anyLock} 0,队列的第一个元素是UUID:threadId,或者是这个队列存在,但是排在队头的第一个元素,是当前这个线程
那么这个条件整体就可以成立了
anyLock和队列,都是不存在的,所以这个条件肯定会成立。接着执行if中的具体逻辑:
- lpop redisson_lock_queue:{anyLock},弹出队列的第一个元素,现在队列是空的,所以什么都不会干
- zrem redisson_lock_timeout:{anyLock} UUID:threadId,从set集合中删除threadId对应的元素,此时因为这个set集合是空的,所以什么都不会干
- hset anyLock UUID:threadId_01 1,加锁成功:
anyLock: {
"UUID_01:threadId_01": 1
} - pexpire anyLock 30000,将这个锁key的生存时间设置为30000毫秒
返回一个nil,在外层代码中,就会认为是加锁成功,此时就会开启一个watchdog看门狗定时调度的程序,每隔10秒判断一下,当前这个线程是否还对这个锁key持有着锁,如果是,则刷新锁key的生存时间为30000毫秒 (看门狗的具体流程上一篇文章有讲述)
客户端B thread02 加锁分析
此时thread01 已经获取到了锁,如果thread02 在10:00:10分来执行加锁逻辑,具体的代码逻辑是怎样执行的呢?
1"while true do "
2+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
3+ "if firstThreadId2 == false then "
4 + "break;"
进入while true死循环,lindex redisson_lock_queue:{anyLock} 0,获取队列的第一个元素,此时队列还是空的,所以获取到的是false,直接退出while true死循环
1"if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
2+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
3"redis.call('lpop', KEYS[2]); " +
4"redis.call('zrem', KEYS[3], ARGV[2]); " +
5"redis.call('hset', KEYS[1], ARGV[2], 1); " +
6"redis.call('pexpire', KEYS[1], ARGV[1]); " +
7"return nil; " +
8"end; " +
此时anyLock这个锁key已经存在了,说明已经有人加锁了,这个条件首先就肯定不成立了;
接着往下执行,看下另外的逻辑:
1"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
2 "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
3 "redis.call('pexpire', KEYS[1], ARGV[1]); " +
4 "return nil; " +
5"end; " +
判断一下,此时这个第二个客户端是UUID_02,threadId_02,此时会判断一下,hexists anyLock
UUID_02:threadId_02,判断一下在anyLock这个map中,是否存在UUID_02:threadId_02这个key?这个条件也不成立
继续执行后续代码:
1"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
2"local ttl; " +
3"if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " +
4 "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" +
5"else "
6 + "ttl = redis.call('pttl', KEYS[1]);" +
7"end; " +
8
9"local timeout = ttl + tonumber(ARGV[3]);" +
10"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
11 "redis.call('rpush', KEYS[2], ARGV[2]);" +
12"end; " +
13"return ttl;",
tonumber() 是lua中自带的函数,tonumber会尝试将它的参数转换为数字。
lindex redisson_lock_queue:{anyLock} 0,从队列中获取第一个元素,此时队列是空的,所以什么都不会有
因为我们是在10:00:10 分请求的,因为anyLock默认过期时间是30s,所以在thread02请求的时候ttl还剩下20s
ttl = pttl anyLock = 20000毫秒,获取anyLock剩余的生存时间,ttl假设这里就被设置为了20000毫秒
timeout = ttl + 当前时间 + 5000毫秒 = 20000毫秒 + 10:00:00 + 5000毫秒 = 10:00:25
接着执行:
zadd redisson_lock_timeout:{anyLock} 10:00:25 UUID_02:threadId_02
在set集合中插入一个元素,元素的值是UUID_02:threadId_02,他对应的分数是10:00:25(会用这个时间的long型的一个时间戳来表示这个时间,时间越靠后,时间戳就越大),sorted set,有序set集合,他会自动根据你插入的元素的分数从小到大来进行排序
继续执行:
rpush redisson_lock_queue:{anyLock} UUID_02:theadId_02
这个指令就是将UUID_02:threadId_02,插入到队列的头部去
返回的是ttl,也就是anyLock剩余的生存时间,如果拿到的返回值是ttl是一个数字的话,那么此时客户端B而言就会进入一个while true的死循环,每隔一段时间都尝试去进行加锁,重新执行这段lua脚本
简单画图总结如下:
image.png
客户端C thread03 加锁分析
此时thread03 在10:00:15来加锁,分析一下执行原理:
1"while true do "
2+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
3+ "if firstThreadId2 == false then "
4 + "break;"
5+ "end; "
6+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
7+ "if timeout <= tonumber(ARGV[4]) then "
8 + "redis.call('zrem', KEYS[3], firstThreadId2); "
9 + "redis.call('lpop', KEYS[2]); "
10+ "else "
11 + "break;"
12+ "end; "
13+ "end;"
while true死循环,lindex redisson_lock_queue:{anyLock} 0,获取队列中的第一个元素,UUID_02:threadId_02,代表的是这个客户端02正在队列里排队
zscore redisson_lock_timeout:{anyLock} UUID_02:threadId_02,从有序集合中获取UUID_02:threadId_02对应的分数,timeout = 10:00:25
判断:timeout <= 10:00:15?,这个条件不成立,退出死循环
1"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
2"local ttl; " +
3"if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " +
4 "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" +
5"else "
6 + "ttl = redis.call('pttl', KEYS[1]);" +
7"end; " +
8
9"local timeout = ttl + tonumber(ARGV[3]);" +
10"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
11 "redis.call('rpush', KEYS[2], ARGV[2]);" +
12"end; " +
13"return ttl;",
firstThreadId获取到的是队列中的第一个元素:UUID_02:thread_02
ttl = 10:00:25 - 10:00:15 = 5000毫秒
timeout = 5000毫秒 + 10:00:15 + 5000毫秒 = 10:00:30
将客户端C放入到对列和有序集合中:
zadd redisson_lock_timeout:{anyLock} 10:00:30 UUID_03:threadId_03
rpush redisson_lock_queue:{anyLock} UUID_03:theadId_03
最终执行完后 如下图:
image.png
Redisson依次加锁逻辑
上面已经知道了,多个线程加锁过程中实际会进行排队,根据加锁的时间来作为获取锁的优先级,如果此时客户端A释放了锁,来看下客户端B、C是如果获取锁的
当客户端A释放锁
客户端B请求获取锁
直接看核心逻辑:
1+ "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
2+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
3"redis.call('lpop', KEYS[2]); " +
4"redis.call('zrem', KEYS[3], ARGV[2]); " +
5"redis.call('hset', KEYS[1], ARGV[2], 1); " +
6"redis.call('pexpire', KEYS[1], ARGV[1]); " +
7"return nil; " +
8"end; " +
if中的判断:
exists anyLock 是否不存在,此时客户端A已经释放锁,所以这个条件成立。
然后判断队列不存在,或者队列中第一个元素为空,此时条件不成立,但是后面是or关联的判断,接着判断队列中的第一个元素是否为当前请求的UUID_02:threadId_02, 如果判断成功则开始加锁。
这里就是公平锁依次加锁的核心逻辑。
申明
本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!
感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫
【分布式锁】02-使用Redisson实现公平锁原理的更多相关文章
- Redisson源码解读-公平锁
前言 我在上一篇文章聊了Redisson的可重入锁,这次继续来聊聊Redisson的公平锁.下面是官方原话: 它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程.所有请 ...
- Java多线程系列--“JUC锁”03之 公平锁(一)
概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...
- Java多线程系列--“JUC锁”04之 公平锁(二)
概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...
- Java多线程系列--“JUC锁”05之 非公平锁
概要 前面两章分析了"公平锁的获取和释放机制",这一章开始对“非公平锁”的获取锁/释放锁的过程进行分析.内容包括:参考代码获取非公平锁(基于JDK1.7.0_40)释放非公平锁(基 ...
- JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,
如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常遇到多个线程访问同一个 ...
- 最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁
在Java并发场景中,会涉及到各种各样的锁如公平锁,乐观锁,悲观锁等等,这篇文章介绍各种锁的分类: 公平锁/非公平锁 可重入锁 独享锁/共享锁 乐观锁/悲观锁 分段锁 自旋锁 01.乐观锁 vs 悲观 ...
- Java锁--非公平锁
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3496651.html 参考代码 下面给出Java1.7.0_40版本中,ReentrantLock和AQ ...
- Java锁--公平锁
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3496147.html 基本概念 本章,我们会讲解“线程获取公平锁”的原理:在讲解之前,需要了解几个基本概 ...
- Java并发指南8:AQS中的公平锁与非公平锁,Condtion
一行一行源码分析清楚 AbstractQueuedSynchronizer (二) 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer ...
随机推荐
- SHELL用法六(Find语句)
1.SHELL编程Find语句案例实战 1)SHELL编程四剑客工具:Find.Grep.Sed.Awk,通过四剑客可以完成常规Linux指令无法完成或者比较复杂的功能,学好SHELL编程四剑客有助于 ...
- WordPress快速打造个人博客
前些天用wordpress搭建了现在这个博客,所以总结了一篇文章,讲讲怎么样简单的创建一个博客.开始前这里有篇我搭建时所遇到的问题可以作为参考<WordPress建站注意事项>,首先我们要 ...
- 接口测试-chap4-关联接口测试
关联接口测试指:请求第2个接口之前必须先请求第一个接口 1.请求第2个接口时需要带着第一个请求返回的cookie 2.如果不使用cookie,如何进行关联接口测试:使用session保持连接.可以代替 ...
- hibernate需要注意的点
1.需要用Hibernate做实体的类(@Entity)需要在配置文件中配置对应的包(例如:spring/appContext-hibernate.xml). 2.hibernateTemplate中 ...
- 在python中使用json
在服务器和客户端的数据交互的时候,要找到一种数据格式,服务端好处理,客户端也好处理,这种数据格式应该是一种统一的标准,不管在哪里端处理起来都是统一的,现在这种数据格式非常的多,比如最早的xml,再后来 ...
- 陪诊App,陪孝子找回人性
2015年开始,越来越多的陪诊App开始上线,纷纷迎来了自己的第一批用户,同时,也迎来了大量资本的青睐,有些名气的问诊app甚至能单轮获得超过1000万元的融资,事实上,投资者是否青睐,简直就是中国新 ...
- ubuntu16.04安装mysql5.6
apt-get install software-properties-commonsudo add-apt-repository 'deb http://archive.ubuntu.com/ubu ...
- app后端用户登录api
app将用户名和密码发送到服务器,服务器验证用户名和密码都正确后,会在redis或memcached服务器中以用户id为键生成token字 符串,然后服务器把token字符串和用户id都返回给客户端( ...
- Sed 实记 · laoless's Blog
sed编辑命令 p 打印匹配行 = 打印文件行号 a 在定位行之后追加文本 i 在定位行之前插入文本 d 删除定位行 c 用新文本替换定位文本 s 使用替换模式替换相应模式 r 从另一个文件读取文本 ...
- iptables学习02-nat表应用
nat表应用实验 第一步 准备工作 A机器两块网卡ens33(192.168.2.106).再添加一块自定义网卡ens37(192.168.100.1),添加到LAN内网区段(自定义名字,写什么无所谓 ...
