面试时遇到「看门狗」脖子上挂着「时间轮」,我就问你怕不怕?
來源 | Why技術
封圖 |? CSDN 下載于視覺中國
之前寫了一篇文章,有一個小節中寫到這樣一段話:
于是就有讀者來問了:老哥,看門狗介紹一下唄。面試的時候被問到了,沒有回答上來。
聽到這個問題我腦海里首先浮現出了幾個問題:
你面試被問到,沒有答上來,然后呢?
面試結束之后你沒有進行面試的復盤嗎?
對于自己沒有回答上來的問題,沒有去進行探索嗎?
甚至你都忘記了當時你的面試題,只是看到我文章的時候,突然想起:哦,這題我之前遇到過,沒有解決。
這個方式是不對的,朋友。
面試后的復盤非常重要
一次面試是一場技術的交鋒,所以面試之后的復盤非常非常的重要,面試結束后的第一件事情就應該是回顧整個面試過程,看看在整個面試的過程中,哪些地方是自己知道但是沒有說清楚的,哪些地方是自己應該知道但是確實不知道,需要去提升的。
然后立刻、馬上、當即在手機標簽或者隨身筆記上記錄下復盤后自己的總結出來的關鍵點。
這些關鍵點可以是表現的好的地方,但是更多的應該是需要提升的地方。
也許你也在網上看到過這個套路:面試的過程中有幾個問題沒有回答上來,最后面試官說你先回去等通知吧。于是面試結束后,你對于沒有回答上來的問題進行了學習,然后把自己的學習總結發給面試官。面試官一看,喲,這小伙可以啊,學習能力還不錯。
然后就真的通知你準備進行下一輪面試吧。
這招我沒用過,但是這個套路,傳遞的思想就是:在自己領域范圍內,不懂的問題,遇到了,你得主動去解決。
面試后的復盤,非常的重要。
復盤過程中的想法形成文字,保留下來,非常非常的重要。
形成文字了,還可以分享出去,幫助后來人。
好了,既然讀者問了這個問題,我就稍微擴展一下,把我自己知道的都分享一下。
先看示例代碼
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) 其實就是我們要找的東西:
你看,這一串東西,不就是我們剛剛看到的 UUID:1 嗎?這個 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]。UUID:thredId 就是 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 后執行的任務,則0+5=5,在第5格的鏈表中添加一個任務節點即可,同時標識該節點round=0。
我們假設它每 1s 轉一格,當前位于第?0?格,現在要添加一個 17s 后執行的任務,則(0+17)% 8 = 1,則在第 1 格添加一個節點指向任務,并標記round=2,時間輪每經過第 1 格后,對應的鏈表中的任務的 round 都會減 1 。則當時間輪第 3 次經過第 1 格時,會執行該任務。
需要注意的是時間輪每次只會執行round=0的任務。
知道了工作原理,我們再看看前面說的 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萬個法人、百萬條銀行賬戶信息,正在暗網兜售真香,朕在看了!總結
以上是生活随笔為你收集整理的面试时遇到「看门狗」脖子上挂着「时间轮」,我就问你怕不怕?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用丁香园数据生成疫情分布地图(R语言)
- 下一篇: 今天下午三点,2020深圳开放数据应用创