Redisson 工作原理-源码分析

时间不在于你拥有多少,而在于你怎样使用

1:Redisson 是什么

个人理解:一种 可重入、持续阻塞、独占式的 分布式锁协调框架,可从 ReentrantLock 去看它。


①:可重入
拿到锁的线程后续拿锁可跳过获取锁的步骤,只进行value+1的步骤。


②:持续阻塞
获取不到锁的线程,会在一定时间内等待锁。

日常开发中,应该都用过redis 的setnx 进行分布式的操作吧,那setnx 返回了false我们第一时间是不是就结束了?
因此redisson 优化了这个步骤,拿不到锁会进行等待,直至timeout 。


③:独占式
很好理解,同一环境下理论上只能有一个线程可以获取到锁。

对于redis 集群模式下,若master 的锁还没有同步给slave,这时 master 挂掉,然后哨兵选举出新的master,
由于新的 master 并没有同步到锁,所以这个时候其他的线程仍然能获取到锁。因此独占式在一定条件下是会失效的。
这个观点在另外几篇参考的文章中也有提到,个人也比较赞同,因此在此写个笔记。

2:示例代码

redisson的GitHub地址:https://github.com/redisson/redisson
我用的是boot-starter,配置参考官网给出的就行了。


测试代码块:

        final String redissonLcokName = "redis-lock";
        final RedissonLock redissonLock = (RedissonLock) redissonClient.getLock(redissonLcokName);

        try {
            redissonLock.lock(100, TimeUnit.SECONDS);
        } catch (Exception ignore) {

        } finally {
            if (redissonLock.isLocked())
                redissonLock.unlock();
        }

是不是和ReentrantLock 很像呢?


贴上我画的草图再讲后面的内容:

在这里插入图片描述

3:如何获取锁

rlock.trylock()     //RLock对象
                 ↓

    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 获取锁过期时间
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
        if (ttl == null) {
            return true;  //ttl 为空,说明锁不存在,获取锁成功
        } else {
        		 
            time -= System.currentTimeMillis() - current;
            // 超过最大等待时间,获取锁失败
            if (time <= 0L) {
                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {  //未超过最大等待时间,继续尝试获得锁
                current = System.currentTimeMillis();
                // 订阅别人释放锁的信号
                RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
                // 等待 剩余时间结束,,获取锁失败
                if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
                    if (!subscribeFuture.cancel(false)) {
                        subscribeFuture.onComplete((res, e) -> {
                            if (e == null) {
                                this.unsubscribe(subscribeFuture, threadId);
                            }

                        });
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                } else {
                    try {
                        time -= System.currentTimeMillis() - current;
                        if (time <= 0L) {
                            this.acquireFailed(waitTime, unit, threadId);
                            boolean var20 = false;
                            return var20;
                        } else {
                            boolean var16;
                            do {
                                long currentTime = System.currentTimeMillis();
                                ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                                if (ttl == null) {
                                    var16 = true;
                                    return var16;
                                }

                                time -= System.currentTimeMillis() - currentTime;
                                if (time <= 0L) {
                                    this.acquireFailed(waitTime, unit, threadId);
                                    var16 = false;
                                    return var16;
                                }

                                currentTime = System.currentTimeMillis();
                                if (ttl >= 0L && ttl < time) {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); //获取别人释放锁的信号量
                                } else {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                                }

                                time -= System.currentTimeMillis() - currentTime;
                            } while(time > 0L);

                            this.acquireFailed(waitTime, unit, threadId);
                            var16 = false;
                            return var16;
                        }
                    } finally {
                        this.unsubscribe(subscribeFuture, threadId);
                    }
                }
            }
        }
    }

Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                 ↓

tryAcquireAsync(waitTime, leaseTime, unit, threadId)   
//  leaseTime != -1 说明传入自己的参数 ; 默认-1
//  若leaseTime != -1 触发看门狗机制
                 ↓
tryLockInnerAsync(threadId) // 执行lua脚本

获取锁的操作采用lua脚本的形式,以保证指令的原子性。

在这里插入图片描述


从截图上的序号来说步骤:
①:如果锁不存在,则进行hincrby 操作(key不存在则value等于1,占锁),并设置过期时间,然后返回nil。
②:如果锁存在且 key 也存在,则进行hincrby操作(可重入锁思想),并以毫秒为单位重新设置过期时间(续命),然后返回nil。
③:如果只存在锁,key 不存在,则说明有其他线程获取到了锁(当前线程需要等待),需要返回锁的过期时间。

在这里插入图片描述


从上述中就可以看出这个锁是 hash 结构的:

在这里插入图片描述

而key的组成应该是:{uuid}:{threadid}
不信?我给你截图...

①:RedissonBaseLock.getLockName(long threadId)

在这里插入图片描述

②:MasterSlaveConnectionManager.MasterSlaveConnectionManager(Config cfg, UUID id)

在这里插入图片描述

4:获取锁成功

4.1:看门狗

看门狗的存在是为了解决任务没执行完,锁就自动释放了场景。
如默认锁的释放时间为30s,但是任务实际执行时间为35s,那么任务在执行到一半的时候锁就被其他线程给抢占了,这明显不符合需求。
因此就出现了看门狗,专门进行续命操作~~

    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 1:获取到锁则返回锁的过期时间,否则返回null
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime != -1) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }

        // 2:任务完成之后执行
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                if (leaseTime != -1) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    // 3:当锁没有预设置释放时间才会调用看门狗线程
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

通过分析底层代码,当锁没有设置自动释放时间才会启用看门狗线程的。
所以我们要预设置过期时间的话最好还是先预估任务的实际执行时间再进行取值为妙...

4.2:时间轮

看门狗的操作实际上就是基于时间轮的。

①:RedissonBaseLock.renewExpiration()

在这里插入图片描述

在此处可以分析到看门狗的执行时间间隔:锁的默认释放时间为30s,因此每10s看门狗就会进行一次续命操作。

在这里插入图片描述


上述代码底层点进去后可以看到实际上用了netty的 HashedWheelTimer 类:

②:MasterSlaveConnectionManager.newTimeout(TimerTask task, long delay, TimeUnit unit)

在这里插入图片描述

功力不够,关于netty的细节就不过多描述了~~


借图说下自己的理解

在这里插入图片描述

如上图为一个时间轮模型,有8个齿轮,指针一秒走一次,那么走完需要8s。

齿轮有两个属性:

task:被执行的任务
bound:当bound = 0 时 task才会被执行,当bound > 0 时,指针每过一次bound - 1 直至为0 。
eg:如果你想31s后执行任务,那么bound应该等于3,齿轮处于第7个位置上。因为3*8+7=31。

4.3:解锁->unlock

rlock.unlock()     //RLock对象
                 ↓
unlockAsync(threadId)
                 ↓
unlockInnerAsync(threadId)   //执行lua脚本将redis中的锁删除
                 ↓
cancelExpirationRenewal(threadId)    // 取消更新任务
(从map中获得锁名,执行removeThreadId(threadId) 、cancel() 方法,最后将锁从map移除)

public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise();
        RFuture<Boolean> future = this.unlockInnerAsync(threadId);
        future.onComplete((opStatus, e) -> {
            this.cancelExpirationRenewal(threadId);
            if (e != null) {
                result.tryFailure(e);
            } else if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
                result.tryFailure(cause);
            } else {
                result.trySuccess((Object)null);
            }
        });
        return result;
    }

unlockInnerAsync(threadId)方法

在这里插入图片描述


①:若当前线程并没有持有锁,则返回nil。
②:当前线程持有锁,则对value-1,拿到-1之后的vlaue。
③:value>0,以毫秒为单位返回剩下的过期时间。(保证可重入)
④:value<=0,则对key进行删除操作,return 1 (方法返回 true)。然后进行redis-pub指令。

redis-pub 之后会被其他获取不到锁的线程给监听到,其他线程又进入下一轮的占锁操作。

5:获取锁失败

5.1:关系类图

这块儿比较麻烦,先给一下比较重要的类图吧...

在这里插入图片描述

5.2:订阅事件

没获取到锁线程后面在干嘛?当然要持续等待啦...
先在redis中发布订阅消息,等待用完锁的线程通知我~

RFuture subscribeFuture = this.subscribe(threadId);     // 订阅
                 ↓

在这里插入图片描述


看看订阅主要干了些啥,从源码上分析一波

①:PublishSubscribe.subscribe(String entryName, String channelName)源码:

unlockInnerAsync方法执行的lua脚本,释放锁后,会publish一条消息通知

    public RFuture<E> subscribe(String entryName, String channelName) {
        AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
        RPromise<E> newPromise = new RedissonPromise<>();
        semaphore.acquire(() -> {
            if (!newPromise.setUncancellable()) {
                semaphore.release();
                return;
            }
            // 1:判断RedisLockEntry 是否存在
            E entry = entries.get(entryName);
            if (entry != null) {
                entry.acquire();
                semaphore.release();
                entry.getPromise().onComplete(new TransferListener<E>(newPromise));
                return;
            }

            // 2:创建RedisLockEntry
            E value = createEntry(newPromise);
            value.acquire();

            E oldValue = entries.putIfAbsent(entryName, value);
            if (oldValue != null) {
                oldValue.acquire();
                semaphore.release();
                oldValue.getPromise().onComplete(new TransferListener<E>(newPromise));
                return;
            }
            // 3:创建一个监听器,别的线程进行redis-pub命令之后进行调用
            RedisPubSubListener<Object> listener = createListener(channelName, value);
            // 4:底层交给netty调用redis-sub命令
            service.subscribe(LongCodec.INSTANCE, channelName, semaphore, listener);
        });

        return newPromise;
    }

②:AsyncSemaphore.acquire(Runnable listener)源码:

public class AsyncSemaphore {
    private final AtomicInteger counter;
    private final Queue<Runnable> listeners = new ConcurrentLinkedQueue<>();
    ...

    public void acquire(Runnable listener) {
        1:将任务假如到阻塞队列
        listeners.add(listener);
        tryRun();
    }

    private void tryRun() {
        if (counter.get() == 0
                || listeners.peek() == null) {
            return;
        }
        // 2:counter>0时tryRun 才会取出linstener 中的任务进行执行
        if (counter.decrementAndGet() >= 0) {
            Runnable listener = listeners.poll();
            if (listener == null) {
                counter.incrementAndGet();
                return;
            }

            if (removedListeners.remove(listener)) {
                counter.incrementAndGet();
                tryRun();
            } else {
                listener.run();
            }
        } else {
            counter.incrementAndGet();
        }
    }
}

③:PubSubLock.createEntry() 源码:

      @Override
    protected RedissonLockEntry createEntry(RPromise<RedissonLockEntry> newPromise) {
        return new RedissonLockEntry(newPromise);
    }


④:RedisLockEntry 的部分源码:

public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {

    private int counter;

    private final Semaphore latch;
    private final RPromise<RedissonLockEntry> promise;
    private final ConcurrentLinkedQueue<Runnable> listeners = new ConcurrentLinkedQueue<Runnable>();

    public RedissonLockEntry(RPromise<RedissonLockEntry> promise) {
        super();
        this.latch = new Semaphore(0);
        this.promise = promise;
    }
    ...
    public Semaphore getLatch() {
        return latch;
    }
}

⑤:RedisPubSubConnection.subscribe(Codec codec, ChannelName... channels)源码:

    public ChannelFuture subscribe(Codec codec, ChannelName... channels) {
        for (ChannelName ch : channels) {
            this.channels.put(ch, codec);
        }
        return async(new PubSubMessageDecoder(codec.getValueDecoder()), RedisCommands.SUBSCRIBE, channels);
    }

    ...
    private <T, R> ChannelFuture async(MultiDecoder<Object> messageDecoder, RedisCommand<T> command, Object... params) {
        RPromise<R> promise = new RedissonPromise<R>();
        // io.netty.Channel
        return channel.writeAndFlush(new CommandData<T, R>(promise, messageDecoder, null, command, params));
    }

给张图可能看起来方便点:

在这里插入图片描述

订阅源码总结:
①:并不是每次每次都会创建RedisLockEntry,理论上是:当前应用内一个channel 对应一个RedisLockEntry 实例。
②:subscribe 的底层是基于netty进行操作的,并不是基于RedisTemplate。
③:不是每次subscribe都会执行到netty层,只有当属于该redis-channel的RedisLockEntry 没有实例化时才会调用到netty层。后续线程的只需要执行RedisLockEntry.acquire 操作即可。

6:redis-pub和redis-sub 是如何遥相呼应的?

6.1:Semaphore.tryAcquire(...)

RedisLockEntry 的latch属性为Semaphore

在这里插入图片描述


我们看看RedisLock.lock() 源码:

在这里插入图片描述

为什么要用while(true) ?

因为只有一个线程能拿到锁啊,如果第一次拿到的ttl=1433ms,那么线程自旋1433ms就够了,但是因为只能有一个线程拿到锁,所以其他线程要进入下一轮的自旋。

红线区域部分会导致当前线程阻塞。
而每次进行subscirbe后,RedisLockEntry.counter 值就会+1,counter值就代表多少线程正在等待获取锁。

6.2:Semaphore.release()

①:RedisPubSubConnection.onMessage(PubSubMessage message) 方法:

    public void onMessage(PubSubMessage message) {
        for (RedisPubSubListener<Object> redisPubSubListener : listeners) {
            redisPubSubListener.onMessage(message.getChannel(), message.getValue());
        }
    }

会调用到下命这个方法 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

②:LockSubPub.onMessage(RedissonLockEntry value, Long message)方法:

    @Override
    protected void onMessage(RedissonLockEntry value, Long message) {
        if (message.equals(UNLOCK_MESSAGE)) {
            Runnable runnableToExecute = value.getListeners().poll();
            if (runnableToExecute != null) {
                runnableToExecute.run();
            }

            // value 就是线程进行subscribe 操作的时候所使用的RedisLockEntry 对象
            // 终于执行release操作了,等待锁的线程可以再次获取锁了
            value.getLatch().release();
        } else if (message.equals(READ_UNLOCK_MESSAGE)) {
            while (true) {
                Runnable runnableToExecute = value.getListeners().poll();
                if (runnableToExecute == null) {
                    break;
                }
                runnableToExecute.run();
            }

            value.getLatch().release(value.getLatch().getQueueLength());
        }
    }

还记得我在上面提到过的吗?下图这个地方

在这里插入图片描述

再往下看就是步骤 LockSubPub.onMessage() 的代码了。

源码总结:

因为进行 redis-sub 之后,当前线程实际上会调用Semaphore.tryAcquire 方法去获取锁的,此处会导致线程自旋(阻塞)一定时间。
而当前线程在进行subscirbe之后因为会添加个 listner 在 RedisPubSubConnection.listeners(阻塞队列中),这个listener 就是用来进行Semaphore.release 的。
当收到redis-pub命令时,先遍历listeners,然后拿到事先传给 linstener 的 RedisLockEntry 实例进行release 操作。
就这样,释放锁-获取锁 就形成了遥相呼应。

看代码和文字看累了直接看图吧:

在这里插入图片描述

7:总结

实属吐槽,个人认为redisson 并不是一个易于源码阅读的框架,看起来很费劲。
主要分为以下几点:
1:调用链太长,参数一直往下传递
2:注释稀少,很难初步看懂某个api具体的职责
3:各种继承关系,写代码的人爽了,看代码的人傻了...

第一批源码文章,实属不易。
最后,如文章有写的不对的地方欢迎各位同学指正。

本文参考文章:
雨点的名字博客:https://www.cnblogs.com/qdhxhz/p/11046905.html
why神的文章:https://juejin.cn/post/6844904106461495303#heading-8

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值