前言
关于读写锁,大家应该都了解JDK中的ReadWriteLock,当然Redisson也有读写锁的实现。
所谓读写锁,就是多个客户端同时加读锁,是不会互斥的,多个客户端可以同时加这个读锁,读锁和读锁是不互斥的Redisson中使用RedissonReadWriteLock来实现读写锁,它是RReadWriteLock的子类,具体实现读写锁的类分别是:RedissonReadLock和RedissonWriteLock。
本文是承接文章续写,大家可以前往阅读:「分布式锁」使用Redisson实现RedLock原理
Redisson读写锁使用例子
还是从官方文档中找的使用案例:
Redisson加读锁逻辑原理
客户端A(UUID_01:threadId_01)来加读锁
注:以下文章中客户端A用:UUID_01:threadId_01标识客户端B用:UUID_02:threadId_02标识
KEYS:
KEYS1:getName()=tryLockKEYS[2]:getReadWriteTimeoutNamePrefix(threadId)={anyLock}:UUID_01:threadId_01:rwlock_timeoutARGV:
ARGV1:internalLockLeaseTime=毫秒ARGV[2]:getLockName(threadId)=UUID_01:threadId_01ARGV[3]:getWriteLockName(threadId)=UUID_01:threadId_01:write接着对代码中lua脚本一行行解读:
hgetanyLockmode第一次加锁时是空的mode=false,进入if逻辑hsetanyLockUUID_01:threadId_anyLock是hash结构,设置hash的key、valueset{anyLock}:UUID_01:threadId_01:rwlock_timeout:11设置一个string类型的keyvalue数据pexpire{anyLock}:UUID_01:threadId_01:rwlock_timeout:1设置keyvalue的过期时间pexpireanyLock设置anyLock的过期时间此时redis中存在的数据结构为:
客户端A第二次来加读锁
继续分析,客户端A已经加过读锁,此时如果继续加读锁会怎样处理呢?
hgetanyLockmode此时mode=read,会进入第二个if判断hincrbyanyLockUUID_01:threadId_此时hash中的value会加1,变成2set{anyLock}:UUID_01:threadId_01:rwlock_timeout:21ind为hincrby结果,hincrby返回是2pexpireanyLockpexpire{anyLock}:UUID_01:threadId_01:rwlock_timeout:2此时redis中存在的数据结构为:
客户端B(UUID_02:threadId_02)第一次来加读锁
基本步骤和上面一直,加锁后redis中数据为:
这里需要注意一下:
为哈希表key中的域field的值加上增量increment,如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。
Redisson加写锁逻辑原理
Redisson中由RedissonWriteLock来实现写锁,我们看下写锁的核心逻辑:
还是像上面一样,一行行来分析每句lua脚本执行语义。
客户端A先加读写、再加写锁
KEYS和ARGV参数:
KEYS1=anyLockARGV1=ARGV[2]=UUID_01:threadId_01:writehgetanyLockmode,此时没人加锁,mode=falsehsetanyLockmodewritehsetanyLockUUID_01:threadId_01:write1pexpireanyLock此时redis中数据格式为:
此时再次来加写锁,直接到另一个if语句中:
hexistsanyLockUUID_01:threadId_01:writehincrbyanyLockUUID_01:threadId_01:write1pexpireanyLockpttl+此时redis中数据格式为:
客户端A和客户端B,先后加读锁,客户端C来加写锁
读锁加完后,此时redis数据格式为:
客户端C参数为:
KEYS1=anyLockARGV1=ARGV[2]=UUID_03:threadId_03:writehgetanyLockmode,mode=read,已经有人加了读锁,不是写锁,此时会直接执行:pttl
anyLock,返回一个anyLock的剩余生存时间
hgetanyLockmode,mode=read,已经有人加了读锁,不是写锁,所以if语句不会成立pttlanyLock,返回一个anyLock的剩余生存时间客户端C加锁失败,就会不断的尝试重试去加锁
客户端A先加写锁、客户端B接着加读锁
加完写锁后此时Redis数据格式为:
客户端B执行读锁逻辑参数为:
KEYS1=anyLockKEYS[2]={anyLock}:UUID_02:threadId_02:rwlock_timeoutARGV1=毫秒ARGV[2]=UUID_02:threadId_02ARGV[3]=UUID_02:threadId_02:write接着看下加锁逻辑:
如上图,客户端B加读锁会走到红框中的if逻辑:
hgetanyLockmode,mode=write客户端A已经加了一个写锁hexistsanyLockUUID_02:threadId_02:write,存在的话,如果客户端B自己之前加过写锁的话,此时才能进入这个分支返回pttlanyLock,导致加锁失败客户端A先加写锁、客户端A接着加读锁
还是接着上面的逻辑,继续分析:
hgetanyLockmode,mode=write客户端A已经加了一个写锁hexistsanyLockUUID_01:threadId_01:write,此时存在这个key,所以可以进入if分支hincrbyanyLockUUID_01:threadId_,也就是说此时,加了一个读锁set{anyLock}:UUID_01:threadId_01:rwlock_timeout:11,pexpireanyLockpexpire{anyLock}:UUID_01:threadId_01:rwlock_timeout:1此时redis中数据格式为:
客户端A先加读锁、客户端A接着加写锁
客户端A加读锁后,redis中数据结构为:
此时客户端A再来加写锁,逻辑如下:
此时客户端A先加的读锁,mode=read,所以再次加写锁是不能成功的
如果是同一个客户端同一个线程,先加了一次写锁,然后加读锁,是可以加成功的,默认是在同一个线程写锁的期间,可以多次加读锁
而同一个客户端同一个线程,先加了一次读锁,是不允许再被加写锁的
总结
显然还有写锁与写锁互斥的逻辑就不分析了,通过上面一些场景的分析,我们可以知道:
读锁与读锁非互斥读锁与写锁互斥写锁与写锁互斥读读、写写同个客户端同个线程都可重入先写锁再加读锁可重入先读锁再写锁不可重入Redisson读写锁释放原理
Redission读锁释放原理
不同客户端加了读锁/同一个客户端+线程多次可重入加了读锁
例如客户端A先加读锁,然后再次加读锁
最后客户端B来加读锁
此时Redis中数据格式为:
接着我们看下释放锁的核心代码:
客户端A来释放锁:对应的KEYS和ARGV参数为:
KEYS1=anyLockKEYS[2]=redisson_rwlock:{anyLock}KEYS[3]={anyLock}:UUID_01:threadId_01:rwlock_timeoutKEYS[4]={anyLock}ARGV1=0ARGV[2]=UUID_01:threadId_01接下来开始执行操作:
hgetanyLockmode,mode=readhexistsanyLockUUID_01:threadId_01,肯定是存在的,因为这个客户端A加过读锁hincrbyanyLockUUID_01:threadId_01-1,将这个客户端对应的加锁次数递减1,现在就是变成1,counter=1del{anyLock}:UUID_01:threadId_01:rwlock_timeout:2,删除了一个timeoutkey此时Redis中的数据结构为:
此时继续往下,具体逻辑如图:
hlenanyLock1,就是hash里面的元素超过1个pttl{anyLock}:UUID_01:threadId_01:rwlock_timeout:1,此时获取那个timeoutkey的剩余生存时间还有多少毫秒,比如说此时这个key的剩余生存时间是毫秒这个for循环的含义是获取到了所有的timeoutkey的最大的一个剩余生存时间,假设最大的剩余生存时间是毫秒
客户端A继续来释放锁:
此时客户端A执行流程还会和上面一直,执行完成后Redis中数据结构为:
因为这里会走counter==0的逻辑,所以会执行
redis.call(hdel,KEYS[1],ARGV[2]);
客户端B继续来释放锁:
客户端B流程也和上面一直,执行完后就会删除anyLock这个key
同一个客户端/线程先加写锁再加读锁
上面已经分析过这种情形,操作过后Redis中数据结构为:
此时客户端A来释放读锁:
hincrbyanyLockUUID_01:threadId_01-1,将这个客户端对应的加锁次数递减1,现在就是变成1,counter=0hdelanyLockUUID_01:threadId_01,此时就是从hash数据结构中删除客户端A这个加锁的记录del{anyLock}:UUID_01:threadId_01:rwlock_timeout:1,删除了一个timeoutkey此时Redis中数据变成:
Redisson写锁释放原理
先看下写锁释放的核心逻辑:
同一个客户端多次可重入加写锁/同一个客户端先加写锁再加读锁
客户端A加两次写锁释放:
此时Redis中数据为:
客户端A来释放锁KEYS和ARGV参数:
KEYS1=anyLockKEYS[2]=redisson_rwlock:{anyLock}ARGV1=0ARGV[2]=ARGV[3]=UUID_01:threadId_01:write直接分析lua代码:
上面mode=write,后面使用hincrby进行-1操作,此时count=1如果count0,此时使用pexpire然后返回0此时客户端A再来释放写锁,count=0hdelanyLockUUID_01:threadId_01:write此时Redis中数据:
后续还会接着判断,如果count=0,代表写锁都已经释放完了,此时hlen如果1,代表加的还有读锁,所以接着执行:
hsetanyLockmoderead
,将写锁转换为读锁
最终Redis数据为:
总结
Redisson陆续也更新了好几篇了,疫情期间宅在家里一直学习Redisson相关内容,这篇文章写了2天,从早到晚。
读写锁这块内容真的很多,本篇篇幅很长,如果学习本篇文章最好跟着源码一起读,后续还会继续更新Redisson相关内容,如有不正确的地方,欢迎指正!