Redis可重入锁的核心流程
Redis可重入锁Demo
创建⼀个maven工程,在pom中引入依赖,本次我们就采用Redisson 3.8.1版本
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.1</version>
</dependency>
如下Demo中Redis的环境采⽤三主三从的⽅式搭建⼀套Redis-Cluster集群环境(搭建 Redis-Cluster集群的步骤这⾥省略,我们重点关注源码层⾯),Demo代码示例如下:
@RestController
public class HelloWordController {
public static void main(String[] args) throws Exception {
//1.配置Redis-Cluster集群节点的ip和port
Config config = new Config();
//redis-cluster集群的ip和port
config.useClusterServers()
.addNodeAddress("redis://192.168.43.xxx:7001")
.addNodeAddress("redis://192.168.43.xxx:7002")
.addNodeAddress("redis://192.168.43.xxx:7003")
.addNodeAddress("redis://192.168.43.xxx:7001")
.addNodeAddress("redis://192.168.43.xxx:7002")
.addNodeAddress("redis://192.168.43.xxx:7003");
//2.通过以上配置创建Redisson的客户端
RedissonClient redisson = Redisson.create(config);
//3.测试Redisson可重⼊锁的加锁、释放锁等功能
testRedissonSimpleLock(redisson);
}
private static void testRedissonSimpleLock(RedissonClient redisson) throws
InterruptedException {
//1.获取key为"anyLock"的锁对象
RLock lock = redisson.getLock("anyLock");
//2.1:加锁
lock.lock();
//2.2:加锁时,设置尝试获取锁超时时间30s、锁超时⾃动释放的时间10s
//lock.tryLock(30, 10, TimeUnit.MILLISECONDS);
Thread.sleep(10 * 1000);
//3.释放锁
lock.unlock();
}
}
Demo⽐较简单,就是我们平常开发⾼频⽤到的⼀些Api,包括加锁和释放锁。
本章讨论以下两个问题
- 客户端线程在底层是如何实现加锁的?
- 定位Master节点
- 加锁
- 如何维持加锁?
可重入锁的加锁机制
具体找哪台机器加锁
首先知道是如何去获取这个锁对象
RLock对象表示⼀个锁对象,这⾥表示我们要对key为anyLock加锁,先获取⼀个锁对象。
带着这个想法,那下⼀步肯定要到redisson.getLock("anyLock")中的getLock⽅法中看下, 如下图所示:
通过源码暂时我们并没有发现有锁定master节点相关的逻辑,但是通过上图我们却发现了这⾥redisson.getLock()⽅法最终获取到的锁对象是RedissonLock,⽽对Redisson的构造中只不过是各种变量的设置⽽已。这里我们先进行记录。如下图:
那既然不在第⼀步中锁定master节点,那么我们就继续看下lock.lock()⽅法,如下图所示:
通过对lock.lock()⽅法的分析,终于我们来到lockInterruptibly()⽅法中,其中发现tryAcquire()⽅法,顾名思义、有点像要获取什么东⻄的意思,进去看看:
通过这里发现加锁的底层逻辑是通过lua脚本的⽅式来实现的。 当然我们看到的lua脚本只是⼀⼤串⻓字符串,它只不过是作为⽅法evalWriteAsync()⽅法的⼀个参数⽽已,所以下⼀步肯定要到evalWriteAsync()⽅法中看下了⽅法内部的逻辑是怎样的:
源码走到这里,有一个getNodeSource()方法,好像就是获取源节点、⽬标master的意思:
走到这里,关于如何通过key锁定Redis-Cluster集群中、某⼀个master节点的问题就找到了。了解过⼀点Redis-Cluster集群基础知识应该都知道,Redis-Cluster集群中的数据分布式是通过⼀个⼀个hash slot⽅式来实现的,Redis-Cluster集群总共16384个hash slot,它们都会被均匀分布到所有master节点上,这⾥计算出key的hash slot之后,就可以通过hash slot 去看⼀看哪个master上有这个hash slot,如果哪个master上有个这个hash slot,那么这个key当然就会落到该master节点上,执⾏加锁指令当然就在该master上执⾏。那这里是如何计算对应的hash slot的呢,我们继续往下看:
通过calcSlot()方法可以看出,⾸先会通过key计算出CRC16值,然后CRC16值对16384进⾏取模,进⽽得到hash slot。
客户端线程首次加锁
由上分析可以看出加锁的逻辑是通过lua脚本进行的
这里的lua脚本有三个参数,KEY[1],ARGV[1],ARGV[2];首先我们分析这三个参数是从哪里获取来的
通过方法提示可以找到这个方法的最后两个参数代表着keys和params,现在分析两个参数
通过前面的获取锁对象走源码分析可以知道这里的KEY[1]对应的就是加锁的key名称
,继续分析剩下两个参数
通过这⾥我们就知道了ARGV[1]的internalLockLeaseTime就是30 * 1000ms,也就是30s了,⽽ARGV[2]的getLockName(threadId)继续分析:
这里发现getLockName(threadId)得到的就是⼀个UUID:ThreadId。
这⾥我们可以理解为⽤UUID来唯⼀标识⼀个客户端,毕竟肯定会有多个客户端的多个线程过来加锁,结合起来UUID:ThreadId当然就是表示、具体哪个客户端上的哪个线程过来加锁,通过这样的组合⽅式能唯⼀标识⼀个线程。
接下来开始分析加锁逻辑:
//判断加锁key是否存在,第一次肯定是不存在的
"if (redis.call('exists', KEYS[1]) == 0) then " +
// hset anyLock UUID:ThreadId 1
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// pexpire anyLock 30000
"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; " +
"return redis.call('pttl', KEYS[1]);"
hset表示给anyLock这个key的hash数据结构添加数据,类似java中的Map,此时的数据结构⼤概就是:
anyLock :
{
UUID:ThreadId:1
}
这样的话,就相当于每个key都有⼀个Map这样的⼀个hash数据结构,使用这种方式保存数据的原因是如果说关于这个key我还要放其他的信息,就可以直接在这个key的hash数据结构中添加key-value对就⾏了
紧接着,pexpire这个就是为anyLock这个key设置下key的过期时间为30s,意思就是30s过后、这个key就会⾃动过期被删除了,当然key对应的锁在那时也就被释放了。 所以我们可以看到,加锁的逻辑也挺简单的,⽆⾮就是在key对应的hash数据结构中记录了⼀下当前是哪个线程过来加锁了,然后设置了⼀下key的过期时间为30s。
加锁成功后如何维持加锁
由上文加锁成功,我们看到脚本返回的是nil
此时加锁成功之后,tryLockInnerAsync()⽅法就返回了,所以下⼀步当然就要再看下调⽤ tryLockInnerAsync()⽅法的位置在⼲些什么,如下图所示:
此时进入到scheduleExpirationRenewal()方法里面去;如下图:
此时就可以判定这个定时任务的执⾏周期是每隔 30 * 1000/3=10*1000ms,也就是每隔10s执⾏⼀次。 继续进入renewExpirationAsync()方法看做了些什么,如下图:
看到这里就可以明白了,原来每次来加锁成功后,监听加锁结果的监听器就会⻢上开启⼀个后台线程, 每隔10s检查⼀下key是否存在,如果存在就为key续期30s。所以⼀个key往往当过期时间慢慢消逝到20s左右时就⼜会被定时任务重置为了30s,这样就能保证、只要这个定时任务还在、只要这个key还在,就能让这个key⼀直存活下去,也就是⼀直维持加锁。 这个定时任务的机制在Redisson中也被称为watchdog看⻔狗机制。
此时整体流程进度如下图所示: