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

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.isHeldByCurrentThread())
redissonLock.unlock();
}

是不是和ReentrantLock 很像呢?


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

3:如何获取锁

获取锁的操作采用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

底层源码:


①:若当前线程并没有持有锁,则返回nil。

②:当前线程持有锁,则对value-1,拿到-1之后的vlaue。

③:value>0,以毫秒为单位返回剩下的过期时间。(保证可重入)

④:value<=0,则对key进行删除操作,return 1 (方法返回 true)。然后进行redis-pub指令。

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

5:获取锁失败

5.1:关系类图

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

5.2:订阅事件

没获取到锁线程后面在干嘛?当然要持续等待啦...

先在redis中发布订阅消息,等待用完锁的线程通知我~


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

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

    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

Redisson 工作原理-源码分析的更多相关文章

  1. guava eventbus 原理+源码分析

    前言: guava提供的eventbus可以很方便的处理一对多的事件问题, 最近正好使用到了,做个小结,使用的demo网上已经很多了,不再赘述,本文主要是源码分析+使用注意点+新老版本eventbus ...

  2. [五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的

      Launcher启动类 本文是双亲委派机制的源码分析部分,类加载机制中的双亲委派模型对于jvm的稳定运行是非常重要的 不过源码其实比较简单,接下来简单介绍一下   我们先从启动类说起 有一个Lau ...

  3. Express工作原理和源码分析一:创建路由

    Express是一基于Node的一个框架,用来快速创建Web服务的一个工具,为什么要使用Express呢,因为创建Web服务如果从Node开始有很多繁琐的工作要做,而Express为你解放了很多工作, ...

  4. Mybatis Interceptor 拦截器原理 源码分析

    Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最 ...

  5. AtomicInteger原理&源码分析

    转自https://www.cnblogs.com/rever/p/8215743.html 深入解析Java AtomicInteger原子类型 在进行并发编程的时候我们需要确保程序在被多个线程并发 ...

  6. Flink中Periodic水印和Punctuated水印实现原理(源码分析)

    在用户代码中,我们设置生成水印和事件时间的方法assignTimestampsAndWatermarks()中这里有个方法的重载 我们传入的对象分为两种 AssignerWithPunctuatedW ...

  7. SharedPreferences 原理 源码 进程间通信 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  8. RedissonLock分布式锁源码分析

    最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等 ...

  9. 源码分析—ThreadPoolExecutor线程池三大问题及改进方案

    前言 在一次聚会中,我和一个腾讯大佬聊起了池化技术,提及到java的线程池实现问题,我说这个我懂啊,然后巴拉巴拉说了一大堆,然后腾讯大佬问我说,那你知道线程池有什么缺陷吗?我顿时哑口无言,甘拜下风,所 ...

  10. Flink中Idle停滞流机制(源码分析)

    前几天在社区群上,有人问了一个问题 既然上游最小水印会决定窗口触发,那如果我上游其中一条流突然没有了数据,我的窗口还会继续触发吗? 看到这个问题,我蒙了???? 对哈,因为我是选择上游所有流中水印最小 ...

随机推荐

  1. windows10操作系统QQ音乐开全局音效后频繁出现报错,鼠标卡顿,系统死机等问题——解决方法

    如题: windows10操作系统QQ音乐开全局音效后频繁出现报错,鼠标卡顿,系统死机等问题. QQ音乐,开启全局音效,提示需要重启: 重启电脑后发现出现频繁卡机,鼠标卡顿,甚至短暂的死机现象,查看控 ...

  2. matplotlab刻度线设置——如何在画布的上下左右四条边框上绘制刻度线

    我们平时使用matplotlib绘图时一般默认的刻度只在画布的右侧和下侧出现,但是在网上看到其他人的绘图往往都是上下左右四个边框线均有刻度,这是如何实现的呢,今天就给出一种设置画布上下左右四条边框刻度 ...

  3. C# 反射以及实际场景使用

    1 什么是反射 首先要复习一下C#的编译过程,可以解释为下图 其中dll/exe中,包括元数据(metadata)和IL(中间语言Intermediate Language) 另外还出现的其他名词:C ...

  4. vue之es6语法

    1.背景 2.let与var与const的区别 <!DOCTYPE html> <html lang="en"> <head> <meta ...

  5. SeaTunnel 发布成为 Apache 顶级项目后首个版本 2.3.2,进一步提高 Zeta 引擎稳定性和易用性

    近日,Apache SeaTunnel 正式发布 2.3.2 版本.此时距离上一版本 2.3.1 发布已有两个多月,期间我们收集并根据用户和开发者的反馈,在 2.3.2 版本中对 SeaTunnel ...

  6. AI阅读助手ChatDOC:基于 AI 与文档对话、重新定义阅读方式的AI文献阅读和文档处理工具

    让 AI 真正成为你的生产力超级助手 AI 时代降临,我们需要积极拥抱 AI 工具 在过去的 2 个多月里,以 ChatGPT 为代表的 AI 风靡全球.随着 GPT 模型的不断优化,ChatGPT ...

  7. 代码随想录Day17

    654.最大二叉树 给定一个不重复的整数数组 nums . 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值. 递归地在最大值 左边 的 子数组前缀 ...

  8. Ubuntu 安装 Docker Engine

    Docker Engine (也称作 Docker CE) 是 Docker 官方的社区版包,它不包含在 Ubuntu 默认的存储库中.因此,你无法直接使用 apt install docker-ce ...

  9. Html 使用scss爆红

      使用     <style  lang="less" scoped> </style>   即可      

  10. ToCom:一次训练随意使用,华为提出通用的ViT标记压缩器 | ECCV 2024

    标记压缩通过减少冗余标记的数量(例如,修剪不重要的标记或合并相似的标记)来加快视觉变换器(ViTs)的训练和推理.然而,当这些方法应用于下游任务时,如果训练和推理阶段的压缩程度不匹配,会导致显著的性能 ...