当前位置:   article > 正文

分布式锁:Redisson源码解析-MultiLock、RedLock_redissonclient.getmultilock

redissonclient.getmultilock

一、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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

二、疑问

在看完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:锁的持有时间。
    • 默认为-1,代表可以无限时长的持有。
  • newLeaseTime:实际的锁持有时间设置,默认等于-1,如果leaseTime不等于-1,如果设置了waitTime,就会设置newLeaseTime=leaseTime × 2,否则等于leaseTime

锁的持有时间在设置了waitTime的情况下翻倍有什么含义

  • time:当前时间,每一次在循环里面获取锁之后,都需要将时间更新
  • remainTime:剩余等待时间,就等于waitTime,每一次在循环里面获取锁之后,就需要减掉(当前时间-time)
  • lockWaitTime:锁实际等待时间,会根据remainTime来计算一个锁时间等待的时间,是一个重载方法获取的数据,可能有别的算法
  • failedLocksLimit:允许获取锁失败的机器数量,也是一个重载方法,有别的用处
流程图

image.png

加锁

加锁实际上是很依赖RedissonLock的,只是做了一些算法逻辑上的控制

  1. 先将要锁定的资源放进一个list里面去维护,方便统一加锁
  2. 再遍历这个list,一个一个的加锁
  3. 实际的加锁就是走的RLock的加锁,增加了如果获取锁失败的话,对允许失败数量和超时时间、锁的持有时间进行的控制
流程图

MultiLock——加锁流程 (2).png

疑问
  1. 如果一个持有的MultiLock锁,在某个时间点,突然有一个锁的redis宕机了,这个时候怎么办?
  2. 其实想想,如果redis宕机了,就会有slave转换成master,此时这个锁如果已经同步过去了,那就没有影响了,如果没有同步过去,会出现什么呢?这样就会出现错误了,锁可能被多个客户端占有了
  3. 感觉很复杂的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 可以看出核心的释放锁就是:lock.unlockAsync()

疑问

  1. 什么是释放锁失败呢?失败后的表现是什么
  2. 释放失败,就是会抛出异常,返回空

四、思考解答

初始化

  • 实际初始化,就很简单,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主服务器

  1. 获取当前时间戳
  1. client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
  • 比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
  1. client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
  1. 如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
  1. 如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/200938
推荐阅读
相关标签
  

闽ICP备14008679号