有用dojo做的网站吗,政务网络及网站建设,台州seo外链推广代理,wordpress仿站视频来源 | Why技术封图 | CSDN 下载于视觉中国之前写了一篇文章#xff0c;有一个小节中写到这样一段话#xff1a;于是就有读者来问了#xff1a;老哥#xff0c;看门狗介绍一下呗。面试的时候被问到了#xff0c;没有回答上来。听到这个问题我脑海里首先浮现出了几个问题有一个小节中写到这样一段话于是就有读者来问了老哥看门狗介绍一下呗。面试的时候被问到了没有回答上来。听到这个问题我脑海里首先浮现出了几个问题你面试被问到没有答上来然后呢面试结束之后你没有进行面试的复盘吗对于自己没有回答上来的问题没有去进行探索吗甚至你都忘记了当时你的面试题只是看到我文章的时候突然想起哦这题我之前遇到过没有解决。这个方式是不对的朋友。面试后的复盘非常重要一次面试是一场技术的交锋所以面试之后的复盘非常非常的重要面试结束后的第一件事情就应该是回顾整个面试过程看看在整个面试的过程中哪些地方是自己知道但是没有说清楚的哪些地方是自己应该知道但是确实不知道需要去提升的。然后立刻、马上、当即在手机标签或者随身笔记上记录下复盘后自己的总结出来的关键点。这些关键点可以是表现的好的地方但是更多的应该是需要提升的地方。也许你也在网上看到过这个套路面试的过程中有几个问题没有回答上来最后面试官说你先回去等通知吧。于是面试结束后你对于没有回答上来的问题进行了学习然后把自己的学习总结发给面试官。面试官一看哟这小伙可以啊学习能力还不错。然后就真的通知你准备进行下一轮面试吧。这招我没用过但是这个套路传递的思想就是在自己领域范围内不懂的问题遇到了你得主动去解决。面试后的复盘非常的重要。复盘过程中的想法形成文字保留下来非常非常的重要。形成文字了还可以分享出去帮助后来人。好了既然读者问了这个问题我就稍微扩展一下把我自己知道的都分享一下。先看示例代码Redisson 分布式锁可能大多数朋友都用过。先上个代码给大家看看是怎么用的。看到这几行代码你先别往下看你先想一想和你自己造的轮子比起来有什么非常明显不一样的地方我给大家分享一下我第一次用 Redission 做分布式锁的时候遇到的两个非常直观的疑问吧。value去哪里了过期时间去哪里了之前说过如果是我们自己造轮子基于 Redis 做分布式锁的话需要向 Redis 发一条下面的命令SET key random_value NX PX 3000
而在我们上面的示例代码中为什么只有 key 没有 value 呢我们知道 value 是必须要有的。还记得《求锤得锤之神仙打架》这篇文章里面说的当面试官问你给我讲一讲基于Redis的加锁和释放锁的细节吧。我们从三个关键点中去回答原子命令加锁。设置值的时候放的是random_value。value 的值设置为随机数主要是为了更安全的释放锁释放锁的时候需要检查 key 是否存在且 key 对应的值是否和我指定的值一样是一样的才能释放锁。所以可以看到这里有获取、判断、删除三个操作为了保障原子性我们需要用 lua 脚本。所以这个 value 是非常重要的。另外第 3 步释放锁的时候为什么需要 lua 脚本也有读者问过其实这事几句话就能说清楚所以我在这里插播一下你看这三个操作获取、判断、删除。获取操作只读不写没有任何问题。问题就出在判断和删除之间。如果不是原子操作出现了下面的情况线程 A 在判断了 value 是自己放进去的在执行 key 删除操作之前程序 GC 导致了 STW。STW 期间线程 A 的锁虽然没有执行删除操作但是由于时间到期被 redis 释放了。STW 之后在线程 A 执行删除操作之前线程 B 加了同样 key 的锁。结果你猜怎么着线程 A 把线程 B 加的锁删除了。这就出问题了。为什么 lua 脚本可以解决这个问题呢因为 lua 脚本的执行是原子性的再加上 Redis 执行命令是单线程的所以在 lua 脚本执行完之前其他的命令都得等着。就不会出现上面说的情况了。第二个问题是过期时间去哪里了呢看上面的加锁代码像是没有设置过期时间似的。我们先说说没有过期时间的问题是什么。很明显嘛容易造成死锁。加锁操作的服务器在没有执行释放锁操作之前服务器崩了。哦豁喜提死锁一把。Value 去哪了对于这个问题首先我们需要确定的是value一定是有的。当我们自己放 value 的时候一般就是搞个随机值往里面一塞就完事了。另外我见过网上有些分析 Redis 分布式锁的文章里面 value 直接扔个 OK 进去。前面我们说过这是不对啊朋友们。要注意辨别。用 Redssion 时我们知道这个 key 肯定是框架帮我们生成了。所以我们只需要去源码中验证我们的想法即可。但是先别慌我们还有一个更加简单的验证方法程序跑起来然后去 Redis 里面看一眼不就完事了看了一眼后发现不错哦不仅验证了我们的想法还有意外收获呢。意外收获一我们看到了 TTL:25 说明虽然我们没用设置过期时间但是框架帮我们把过期时间设置好了。这部分在这一小节中先按下不表等下一小节详细描述。意外收获二可以看到我们放进去的 why 是一个 Hash 类型。并不是我们常用的 String 类型。很明显key 是 UUID:1这个 1 是什么含义呢为什么要用 Hash 类型而不用 String 类型呢我们带着这两个疑问去看一眼源码。注意本文中的 Redssion 的 Maven 版本为 3.12.3。Redssion 的源码非常好 Debug我建议你自己实际操作一遍。首先 lock 操作会调用到这个方法org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)可以看到在这里的时候获取到的 thredId 就是 1。那 key 里面 UUID 后面拼接的 1。是不是就是这里来的呢我们接着往下看。再往前 Debug 三步就能到下面的这个位置org.redisson.RedissonLock#tryLockInnerAsync到这里的 getLockName(threadId) 其实就是我们要找的东西你看这一串东西不就是我们刚刚看到的 UUID1 吗这个 1 就是线程ID。什么你问我为什么说这个 id 是 UUID直觉程序猿的直觉告诉我这就是个 UUID。但是我可以给你验证一下。这个 id 的来源是下面这个接口org.redisson.connection.ConnectionManager而该接口有 5 个实现类在创建 ConnectionManager 时每个实现类的构造方法传的都是 UUID。所以我们可以下结论了使用 Redssion 做分布式锁不需要明确指定 value 框架会帮我们生成一个由 UUID 和 加锁操作的线程的 threadId 用冒号拼接起来的字符串。毫无挑战甚至有点无聊的探索过程啊。其实我想表达的是源码真的不难不要抱有恐惧的心理带着问题去看源码。但是别着急这只是开胃菜。对于第二个问题为什么要用 Hash 类型而不用 String 类型呢我们在下一节寻找过期时间去哪里了的同时寻找该问题的答案。过期时间去哪了这个问题我们从这段代码里面可以找到答案org.redisson.RedissonLock#tryLockInnerAsync我们首先看一下这个方法对应的几个入参主要关注我框起来的部分script是要执行的 lua 脚本。keys是 redis 中的 key。这里的 why 就是 KEYS[1]。params是 lua 脚本的参数。这里的 30000 就是 ARVG[1]。UUIDthredId 就是 ARVG[2]。所以这个过期时间我们也知道了默认是 30000ms即30s。知道了上面三个参数的含义后我们再来拆解这个 lua 脚本就很简单了首先我们把他拆解为三部分第一部分加锁先看第一部分的加锁操作第4行首先用 exists 判断了 KEYS[1] 即 why是否存在。如果不存在则进入第 5 行使用 hincrby 命令。hincrby 命令是干什么的知道吧之后进入第 6 行对 KEY[1] 设置过期时间30000ms。然后第7行进行返回为 nil结束。这样一个原子性的加锁操作就完成了。到这里我们就已经从源码的角度验证了因为用的是 hincrby 命令Redssion 做锁的时候 key 确实是一个 Hash 结构。第二部分重入当第一部分的 if 分支判断 KEYS[1] 是存在的则会进入到这个分支中由于 KEYS[1] 是一个 Hash 结构所以第 13 行的意思是获取这个 KEYS[1] 中字段为 ARGV[2] 的数据判断是否存在。如果存在则进入第 14 行代码用 hincrby 命令对 ARGV[2] 字段进行加一操作。然后第 15 行没啥说的就是重新设置过期时间为 30s。之后第 16 行返回为 nil结束。所以你在感受一下第 14 行代码的作用是什么进入然后加一你联想到了什么看到这里的时候解锁的 lua 脚本都不必看的想也能想到肯定是有一个减一的操作然后减到 0就释放这把锁。一会我们就去验证这个点。所以这里也就解释了为什么 Redssion 需要用 Hash 类型做锁。因为它支持可重入呀。你用 String 类型你怎么实现重入功能来键盘给你实现一个让我学习一下其实也是可以的就是有点背道而驰了。没意义。第三部分返回一行代码问题不大。作用就是返回 KEY[1] 的剩余存活时间。通过分析 lua 的这三部分我们知道了过期时间默认是 30s。当一个 key 加锁成功或者当一个锁重入成功后都会返回空只有加锁失败的情况下会返回当前锁剩余的时间。记住这个结论我们在接下来的看门狗咋工作的这一小节中会用到这个返回值。另外写文章的时候我发现 Redssion 的最新版本 3.12.3 和之前的版本相比加锁时的 lua 脚本有一个细微的差别如下3.12.3 版本之前用的是 hset 现在用的是 hincrby。所以导致第一部分和第二部分相似度有点高。看起来会有点容易迷糊。你去网上找应该看到的都是说 hset 操作的。因为 3.12.3 版本刚刚发布一个月。恭喜你朋友又学到了一个用不上的知识点。看门狗咋工作的看到这一节的朋友们辛苦了。在这一节我们终于要看到看门狗长啥样了。org.redisson.RedissonLock#tryAcquireAsync这里的 ttlRemaining 就是经过 lua 脚本后返回的值。经过前面我们知道了当加锁成功或者重入成功后会返回 null。进入这个方法org.redisson.RedissonLock#scheduleExpirationRenewal这个方法就是看门狗工作的片区了。Debug之后你会遇到这个方法org.redisson.RedissonLock#renewExpiration很明显从上面标注的数字可以看出来①这是一个任务。②这任务需要执行的核心代码。③该任务每 internalLockLeaseTime/3ms 后执行一次。而 internalLockLeaseTime 默认为 30000。所以该任务每 10s 执行一次。接着我们看一下 ② 里面执行的核心代码是什么这个 lua 脚本先判断 UUID:threadId 是否存在如果存在则把 key 的过期时间重新设置为 30s这就是一次续命操作。来在做个小学二年的算法题应用题key 默认的过期时间是 30s每过 30s/3 的时候会去进行续命操作那么每当 key 的 ttl剩余时间返回多少的时候会进行续命操作答由题干可知30s/3 10s。于是得公式到30s - 10s 20s。所以每当 key 的 ttl剩余时间为 20 的时候则进行续命操作重新将 key 的过期时间设置为默认时间 30s。注意我上面一直强调的是默认时间 30s。因为这个时间是可以修改的比如我们想要修改为 60s就这样于是 internalLockLeaseTime 就变成了 60000 了那么附加题就来了。附加题阅读上面材料后当默认时间被修改为 60s 后那么每当 key 的 ttl剩余时间 返回多少的时候会进行续命操作答由题可得时间每过 60s/3 20s 时任务会被触发看门狗进行工作。所以60s -20s 40s。每当 key 的 ttl 返回 40 时会进行续命操作。得学会变形朋友们明白吗接下来我们看看这个 task 任务是怎么实现的。可以看到这个 Timeout 是 netty 包里面的类。这个 task 任务是基于 netty 的时间轮做的。面试官追问你啥是时间轮你又不知道。那你接着往下看。时间轮又是啥你听到了时间轮你首先想到了啥听到这个词就算你完全不知道时间轮你也该想到轮子嘛不就是一个环嘛。网上随便一搜你就知道它确实长成了一个环状它的工作原理如下图片中的时间轮大小为 8 格每格又指向一个保存着待执行任务的链表。我们假设它每 1s 转一格当前位于第 0 格现在要添加一个 5s 后执行的任务则055在第5格的链表中添加一个任务节点即可同时标识该节点round0。我们假设它每 1s 转一格当前位于第 0 格现在要添加一个 17s 后执行的任务则017% 8 1则在第 1 格添加一个节点指向任务并标记round2时间轮每经过第 1 格后对应的链表中的任务的 round 都会减 1 。则当时间轮第 3 次经过第 1 格时会执行该任务。需要注意的是时间轮每次只会执行round0的任务。知道了工作原理我们再看看前面说的 Timeout 类其实就是 HashedWheelTimer 里面 newTimeout 方法的返回前面我们分析了在 Redssion 实现看门狗功能的时候使用的是 newTimeout 方法。该方法三个入参task任务对于 Redssion 看门狗功能来说这个 task 就是把对应的 key 的过期时间重置默认是 30s。delay每隔多久执行一次对于 Redssion 看门狗功能来说这个 delay 就是 internalLockLeaseTime/3 算出来的值默认是 10s。unit时间单位。其实你发现了吗这个时候我们已经脱离了 Redssion 进入 Netty 了。我们只需要告诉 newTimeout 方法我们要每隔多少时间执行一次什么任务就行。那我们为什么不自己写个更加简单的易于理解的 Demo 来分析这个时间轮呢比如下面这样的上面的 Demo 应该是很好理解了。到这里我们知道了看门狗是基于定时任务实现的而这个定时任务是基于 Netty 的时间轮实现的。对于 HashedWheelTimer 的源码开始我还想进行一个导读写着写着去查阅资料的时候发现这个链接里面的对于源码的解读已经很到位了我索性把自己的写那部分删除了大家有兴趣的可以去阅读一下https://www.jianshu.com/p/1eb1b7c67d63另外关于时间轮还可以看一下 IBM 论坛里面的这篇文章《浅析 Linux 中的时间编程和实现原理》https://www.ibm.com/developerworks/cn/linux/1308_liuming_linuxtime3/index.html解锁操作还记得我们加锁操作的时候说的吗进入然后加一你联想到了什么 这不就是可重入锁吗 看到这里的时候解锁的 lua 脚本都不必看的想也能想到肯定是有一个减一的操作然后减到 0就释放这把锁。一会我们就去验证这个点。这一小节我们就去验证这个点请看下面的释放锁执行的 lua 脚本是不是里面有个 counter 的判断如果减一后小于等于 0。就执行 del key 的操作。解锁操作确实挺简单主要是 del 之后执行了一个 publish 命令。你猜这里 publish 的是啥先猜再验证嘛大胆假设小心求证这里是基于 redis 的发布/订阅功能。解锁的时候发布了一个事件你觉得通知的是什么玩意肯定是告诉别的线程我这边锁用完了你来获取吧。别的线程是什么线程呢就是想要申请同一把锁的线程。tryAcquire 的代码我们之前分析过当 ttl 不为 null 时只有一种情况那就是加锁失败所以加锁失败的线程就执行了 subscribe 方法完成了订阅。这样就和释放锁时的 publish 操作呼应上了。接下来就只剩下一个问题没有解决了怎么让看门狗知道不用续命了其实就是在执行完解锁的 lua 脚本之后通过响应式编程完成了 cancel 操作。自此我们的加锁、看门狗续命、解锁的一套操作就完成了。补充说明顺便打脸在打脸之前我先问个问题吧看门狗什么情况下会失效别给我说宕机宕机之后由于线程没了看门狗只是不续命了 redis 里面的 key 到期之后就删除了。我问的失效是指什么时候完全就不启动答案是调用 lock 方法的时候传进一个指定时间这样如果指定时间之内没有调用 unLock 方法该锁还是会被释放的。就像下面这样rLock.lock(5,TimeUnit.SECONDS);该锁在 5s 之后就会自动释放了。不会进行续命操作对应的源码如下注意看我写的注释所以我想起很久之前我在群里说的这个红框框起来的部分是错的明确指定了超时时间的时候是不会启动看门狗机制。自己打自己脸的事......好爽啊这事我经常干。而且读书人的事这能叫打脸吗这叫成长。另外这图画的挺好的分享给大家图片来源https://juejin.im/post/5bf3f15851882526a643e207还有一个读者提出的问题续租的时候是否需要进行次数的限制我觉得是不需要限制的如果你的代码一直在进行续期操作说明两种情况由于某种异常原因导致你本次需要处理的数据比之前的多所以需要的时间更长导致一直在进行续期操作。你的代码有问题导致了死循环也就是死锁的出现这个锅Redssion 不背。最后还有一个问题这锁安全吗或者说你觉得会有什么问题什么你不知道之前分享过的文章中说过了节点之间异步通信会出现上面描述的情况。所以 Redis 推出的解决方案是啥RedLock。其实后来有一天我突然想到 如果从 CAP 的角度上去看 Redis 分布式锁问题我觉得可能更好理解一点。分布式锁的一致性要求 CP但是 Redis 集群架构之间的异步通信满足的是 AP 因此对不上呀就是有问题的啊。 但是为什么 Redis 做分布式锁还是那么流行呢可能是因为大多场景中可以容忍它的这个问题也可能是使用者存在侥幸心理吧或者说使用者就当个黑盒使用根本不知道可能会出问题。同时欢迎所有开发者扫描下方二维码填写《开发者与AI大调查》只需2分钟即可收获价值299元的“ AI开发者万人大会”在线直播门票推荐阅读很用心的为你写了 9 道 MySQL 面试题建议收藏
小网站的容器化下网站容器化的各种姿势先跟着撸一波代码再说克隆一个 AI 替自己开会爽吗以太坊2.0中的Custody Game及MPC实现程序员“我放弃了年薪 20 万的 Offer”20万个法人、百万条银行账户信息正在暗网兜售真香朕在看了