面試時遇到『看門狗』脖子上掛着『時間輪』,我就問你怕不怕? | 掘金技術徵文

Redisson的看門狗和Netty的時間輪,瞭解一下?寫的過程當中順便打了一下本身的臉。技術嘛,不就是在不斷打臉的過程當中成長起來的嘛。html

荒腔走板聊生活

你們好,一週的時間過的飛快,轉眼間又到週末了。java

老規矩,仍是本號特點,先是荒腔走板的聊聊生活。linux

上面的圖片是我在一次跑步的過程當中拍的,一隻狗子。能夠看到圖片中還有一個軌跡圖,也是一隻狗子。面試

這個軌跡圖全長21km,恰好是一個半馬的距離,並且一路上會穿過北海、什剎海、南鑼鼓巷、雍和宮、地壇、鼓樓大街、德勝門這些比較知名的景點。我我的很是喜歡這個路線,在北京的時候跑過好幾回。redis

若是你在北京,也有去跑一跑這個軌跡而後發朋友圈裝逼的想法,你能夠私信我,我給你路線圖。算法

最近因爲疫情,距離上次跑步已通過了好久好久了,雖然公司有跑步機,可是我一貫不太習慣在跑步機上跑。編程

這周成都基本上解禁了,小區裏面跑步的人也慢慢出來了,因此我決定這篇文章寫完後,必定要出去跑個至少10km,準備開啓這個夏天,畢竟一到夏天,我不經意間漏出的腹肌也該出來活動活動了。安全

好了,說迴文章。服務器

面試後的覆盤很是重要

以前寫了《求錘得錘之神仙打架》這篇文章,在長髮哥出錘這一小節中,裏面有寫到這樣一段話:markdown

因而就有讀者來問了:老哥,看門狗介紹一下唄。面試的時候被問到了,沒有回答上來。

聽到這個問題我腦海裏首先浮現出了幾個問題:

1.你面試被問到,沒有答上來,而後呢?

2.面試結束以後你沒有進行面試的覆盤嗎?

3.對於本身沒有回答上來的問題,沒有去進行探索嗎?

甚至你都忘記了當時你的面試題,只是看到我文章的時候,忽然想起:哦,這題我以前遇到過,沒有解決。

這個方式是不對的,朋友。

一次面試是一場技術的交鋒,因此面試以後的覆盤很是很是的重要,面試結束後的第一件事情就應該是回顧整個面試過程,看看在整個面試的過程當中,哪些地方是本身知道可是沒有說清楚的,哪些地方是本身應該知道可是確實不知道,須要去提高的。

而後馬上、立刻、立即在手機標籤或者隨身筆記上記錄下覆盤後本身的總結出來的關鍵點。

這些關鍵點能夠是表現的好的地方,可是更多的應該是須要提高的地方。

也許你也在網上看到過這個套路:面試的過程當中有幾個問題沒有回答上來,最後面試官說你先回去等通知吧。因而面試結束後,你對於沒有回答上來的問題進行了學習,而後把本身的學習總結髮給面試官。面試官一看,喲,這小夥能夠啊,學習能力還不錯。

而後就真的通知你準備進行下一輪面試吧。

這招我沒用過,可是這個套路,傳遞的思想就是:在本身領域範圍內,不懂的問題,遇到了,你得主動去解決。

面試後的覆盤,很是的重要。

覆盤過程當中的想法造成文字,保留下來,很是很是的重要。

造成文字了,還能夠分享出去,幫助後來人。

好了,既然讀者問了這個問題,我就稍微擴展一下,把我本身知道的都分享一下。

先看示例代碼

Redisson 分佈式鎖可能大多數朋友都用過。先上個代碼給你們看看是怎麼用的。

看到這幾行代碼,你先別往下看,你先想想,和你本身造的輪子比起來有什麼很是明顯不同的地方?

我給你們分享一下我第一次用 Redission 作分佈式鎖的時候遇到的兩個很是直觀的疑問吧。

1.value去哪裏了?

2.過時時間去哪裏了?

以前說過,若是是咱們本身造輪子,基於 Redis 作分佈式鎖的話,須要向 Redis 發一條下面的命令:

SET key random_value NX PX 3000

而在咱們上面的示例代碼中,爲何只有 key 沒有 value 呢?

咱們知道 value 是必需要有的。還記得《求錘得錘之神仙打架》這篇文章裏面說的,當面試官問:

你給我講一講基於Redis的加鎖和釋放鎖的細節吧。

咱們從三個關鍵點中去回答:

1.原子命令加鎖。

2.設置值的時候,放的是random_value。

3.value 的值設置爲隨機數主要是爲了更安全的釋放鎖,釋放鎖的時候須要檢查 key 是否存在,且 key 對應的值是否和我指定的值同樣,是同樣的才能釋放鎖。因此能夠看到這裏有獲取、判斷、刪除三個操做,爲了保障原子性,咱們須要用 lua 腳本。

因此,這個 value 是很是重要的。

另外,第 3 步,釋放鎖的時候爲何須要 lua 腳本,也有讀者問過,其實這事幾句話就能說清楚,因此我在這裏插播一下:

你看這三個操做:獲取、判斷、刪除。

獲取操做,只讀不寫,沒有任何問題。問題就出在判斷和刪除之間。若是不是原子操做,出現了下面的狀況:

1.線程 A 在判斷了 value 是本身放進去的,在執行 key 刪除操做以前,程序 GC 致使了 STW。

2.STW 期間線程 A 的鎖雖然沒有執行刪除操做,可是因爲時間到期被 redis 釋放了。

3.STW 以後,在線程 A 執行刪除操做以前,線程 B 加了一樣 key 的鎖。

4.結果你猜怎麼着?線程 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 方法。該方法三個入參:

1.task,任務,對於 Redssion 看門狗功能來講,這個 task 就是把對應的 key 的過時時間重置,默認是 30s。

2.delay,每隔多久執行一次,對於 Redssion 看門狗功能來講,這個 delay 就是 internalLockLeaseTime/3 算出來的值,默認是 10s。

3.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.cn/post/6844903717641142285

還有一個讀者提出的問題,續租的時候,是否須要進行次數的限制?

我以爲是不須要限制的,若是你的代碼一直在進行續期操做,說明兩種狀況:

1.因爲某種異常緣由,致使你本次須要處理的數據比以前的多,因此,須要的時間更長,致使一直在進行續期操做。

2.你的代碼有問題,致使了死循環,也就是死鎖的出現,這個鍋,Redssion 不背。

最後,還有一個問題,這鎖安全嗎,或者說你以爲會有什麼問題?

什麼?你不知道?

以前分享過的文章中說過了:

節點之間異步通訊,會出現上面描述的狀況。因此 Redis 推出的解決方案是啥?

RedLock,以前寫的《求錘得錘之神仙打架》這篇文章,就是講 RedLock 的。若是你不知道,你就去瞅一眼。

其實後來有一天我忽然想到, 若是從 CAP 的角度上去看 Redis 分佈式鎖問題,我以爲可能更好理解一點。

分佈式鎖的一致性要求 CP,可是Redis 集羣架構之間的異步通訊知足的是 AP ,所以對不上呀,就是有問題的啊。

可是爲何 Redis 作分佈式鎖仍是那麼流行呢?

多是由於大多場景中能夠容忍它的這個問題,也多是使用者存在僥倖心理吧,或者說使用者就當個黑盒使用,根本不知道可能會出問題。

最後說一句(求關注)

寫完以後一看時間又是凌晨 2 點過了:

點個「在看」吧,周更很累的,不要白嫖我,須要一點正反饋。

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。(我每篇技術文章都有這句話,我是認真的說的。)

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是why技術,一個不是大佬,可是喜歡分享,又暖又有料的四川好男人。

歡迎關注公衆號【why技術】,堅持輸出原創。分享技術、品味生活,願你我共同進步。

相關文章
相關標籤/搜索