一、MultiLock
Github
我们从官方文档开始来看看 文档地址,总结一下就下面几点了:
- Redis基于分布式的
MultiLock 对象,实现了一组锁对象合并成一个大锁,统一进行加锁和释放锁,每个锁对象可能属于不同的redisson实例 - 存在一种可能,如果获取到
MultiLock 实例挂掉了,那么就可能导致这个multiLock一直处于被持有的状态,所以可以设置leaseTime和waitTime - 符合Java Lock规范,只有锁的持有者可以释放他
代码实现
// 初始化三个锁
RLock lock1 = redissonClient.getLock("lockName1");
RLock lock2 = redissonClient.getLock("lockName2");
RLock lock3 = redissonClient.getLock("lockName3");
// 初始化三个锁的合并锁
RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);
// 获取锁
multiLock.lock();
// 释放锁
multiLock.unlock();
二、疑问
在看完BaseLock和FairLock之后,会冒出一些需要关注的点
1. 初始化
从代码中可以看到,他是先初始化的三个RLock锁,然后再初始化的MultiLock,那么这个初始化过程做了什么呢?会有所好奇的
初始化的工作做了什么
2. 加锁
针对加锁这一块就会疑问比较多了,因为必须考虑到分布式锁的特性是如何去实现的?
锁分组
既然是批量统一的锁住一批资源,且看代码中是先获取的多个RLock,那么这些锁是如何去进行的分组呢?需要不需要分组呢?如何做到统一锁定资源的?
加锁流程
整个加锁的过程是一个什么样子的?
锁的数量
最终Redis中存留的会有几个锁呢?
猜测应该是三个,只是和有类似CountDownLatch的机制,等待三个同时获取到锁,才会说获取锁成功,加锁成功返回
可重入锁
首先,这个锁是不是可重入锁?如果是可重入锁是如何去进行控制的呢?
锁的维持
在加锁成功之后,又是怎么样来维持的这个锁呢?如果是加锁三个的话,那不是得有三个watchdog在运行?
锁互斥
锁互斥倒是好理解,可以知道如果说都获取到了锁,也就是说统一资源加锁成功,那么这些资源就都被锁到了其他线程会加锁失败
锁阻塞
有锁阻塞吗,其实每一个分组锁定的资源都是阻塞的形式
获取锁超时
如果获取锁超时,那么其他已经获取到锁的客户端,会怎么停留?直到超时了,已经获取到锁的客户端怎么办呢?
加锁失败
如何判定加锁失败,是所有的都得成功吧
3. 释放锁
被动释放
被动释放锁,应该分成几种吧
- redisson客户端宕机了,锁被释放
- 如果是一个客户端获取锁失败,应该也会导致其他锁的资源是放掉
主动释放
主动释放,也就是做完了工作自己来释放
三、解析
1. 初始化
获取MultiLock的时候,实际是获取的RedissonMultiLock 锁对象,接收的参数是前面初始化的RLock对象,MultiLock内部维护了一个RLock List属性
也就是说,这里的实际初始化是初始化了一个RLock列表
2. 加锁
赋值与初始化
- waitTime:锁的获取等待时间。
-
- 如果设置了-1,则表示没有等待时间,失败了就是失败了。
- 默认调用lock方法的时候是会设置一个初始值的,baseWaitTime=locks.size() * 1500
- 如果没有设置leaseTime,就会等于这个baseWaitTime,如果设置了lease,则根据lease的值来控制
- 如果大于leaseTime值小于2000,则会直接设置为2000
- 如果leaseTime的值大于2000且小于baseWaitTime,则会设置为在 baseWaitTime/2~baseWaitTime之间选取一个随机值
- 如果leaseTime的值大于baseWaitTime,则设置为baseWaitTime~leaseTime之间的一个随机值
waitTime这样设置有什么好处呢?
- leaseTime:锁的持有时间。
- newLeaseTime:实际的锁持有时间设置,默认等于-1,如果leaseTime不等于-1,如果设置了waitTime,就会设置newLeaseTime=leaseTime × 2,否则等于leaseTime
锁的持有时间在设置了waitTime的情况下翻倍有什么含义
- time:当前时间,每一次在循环里面获取锁之后,都需要将时间更新
- remainTime:剩余等待时间,就等于waitTime,每一次在循环里面获取锁之后,就需要减掉(当前时间-time)
- lockWaitTime:锁实际等待时间,会根据remainTime来计算一个锁时间等待的时间,是一个重载方法获取的数据,可能有别的算法
- failedLocksLimit:允许获取锁失败的机器数量,也是一个重载方法,有别的用处
流程图
加锁
加锁实际上是很依赖RedissonLock的,只是做了一些算法逻辑上的控制
- 先将要锁定的资源放进一个list里面去维护,方便统一加锁
- 再遍历这个list,一个一个的加锁
- 实际的加锁就是走的RLock的加锁,增加了如果获取锁失败的话,对允许失败数量和超时时间、锁的持有时间进行的控制
流程图
疑问
- 如果一个持有的MultiLock锁,在某个时间点,突然有一个锁的redis宕机了,这个时候怎么办?
- 其实想想,如果redis宕机了,就会有slave转换成master,此时这个锁如果已经同步过去了,那就没有影响了,如果没有同步过去,会出现什么呢?这样就会出现错误了,锁可能被多个客户端占有了
- 感觉很复杂的waitTime和leaseTime的机制?类似,leaseTime == waitTime * 2,waitTime等于lock.size * 1500
3. 释放锁
释放锁的整个流程就比较简单了,几乎是把RLock中释放锁的逻辑代码给抄过来的,就是做了一个遍历
protected RFuture<Void> unlockInnerAsync(
Collection<RLock> locks, long threadId) {
if (locks.isEmpty()) {
return RedissonPromise.newSucceededFuture(null);
}
RPromise<Void> result = new RedissonPromise<Void>();
AtomicInteger counter = new AtomicInteger(locks.size());
for (RLock lock : locks) {
lock.unlockAsync(threadId).onComplete((res, e) -> {
if (e != null) {
result.tryFailure(e);
return;
}
if (counter.decrementAndGet() == 0) {
result.trySuccess(null);
}
});
}
return result;
}
- 可以看出核心的释放锁就是:lock.unlockAsync()
疑问
- 什么是释放锁失败呢?失败后的表现是什么
- 释放失败,就是会抛出异常,返回空
四、思考解答
初始化
- 实际初始化,就很简单,MultiLock的初始化就是将多个要统一管理的资源RLock给统一管理,初始化一个list来控制
锁分组
- 锁分组其实是一个算法逻辑的概念,主要就是通过批量的获取RLock锁
加锁流程
- 加锁的过程是很简单的,就是遍历调用RLock里面的方法,通过统一控制锁的超时,锁的获取成功失败数量比来控制加锁的结果
锁的数量
- 看完整个逻辑代码,其实就很清楚了,MultiLock其实就是几个资源几个锁
可重入锁
- 本质不能说MultiLock是可重入锁,应该说RLock是可重入锁,实际的加锁也是走的RLock的流程
锁的维持
- 这个就更是了,和RLock是一样的,都是通过watchdog来运行的,每个锁的都会有一个watchdog
锁互斥、锁阻塞
- 像批量锁还好一点,规定了不允许有获取失败的锁存在,如果有失败的,那么就要重头开始来
- 只要几个被锁住的资源,还在持有锁的状态,其他锁不会再成功获取这个小锁
锁超时
- 会维护一个remainTIme,每一次获取锁之后,都会对这个值进行递减
加锁失败
- 里面维护了一个最小失败数量,如果失败的数量到达这么多就会直接失败掉
- 或者是remainTime到了,超时了也会加锁失败
主动释放锁
- 成功获取到了MultiLock,然后通过unlock来进行释放。走的是RLock的释放流程
- 还有一种,在获取MultiLock的路上,中间有一个失败了,如果没有超时,就需要从头在来,把获取到的锁给是放掉
被动释放锁
- 在获取MultiLock的路上,然后客户端宕机了,就会直接等待redis中的key超时了,才能加锁成功
- 在获取到MultiLock以后,redis master宕机了,就很危险了,如果已经异步复制过去了,就不会有什么问题,如果还没复制过去,就玩完,直接多客户端加锁成功
六、RedLock
前面我们讲到,如果说master宕机的情况下,由于是异步复制的,可能存在key没有复制到slave上,就死掉了,就会导致了多客户端同时加锁成功的可能性
Github
This object is deprecated. RLock operations now propagated to all Redis slaves.
弃用了……
从使用的角度来看,是继承自MultiLock的, 重写了其中的几个方法
failedLocksLimit--> lock.size - (locks.size()/2 + 1) calcLockWaitTime --> Math.max(remainTime / locks.size(), 1)
多节点redis实现的分布式锁算法(RedLock):有效防止单点故障
算法
假设有5个完全独立的redis主服务器
- 获取当前时间戳
- client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
- 比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
- client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
- 如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
- 如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
|