<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          舒服了,踩到一個(gè)關(guān)于分布式鎖的非比尋常的BUG!

          共 8752字,需瀏覽 18分鐘

           ·

          2022-05-20 08:46


          提到分布式鎖,大家一般都會(huì)想到 Redis。

          想到 Redis,一部分同學(xué)會(huì)說到 Redisson。

          那么說到 Redisson,就不得不掰扯掰扯一下它的“看門狗”機(jī)制了。

          所以你以為這篇文章我要給你講“看門狗”嗎?

          不是,我主要是想給你匯報(bào)一下我最近研究的由于引入“看門狗”之后,給 Redisson 帶來的兩個(gè)看起來就心里一緊的 bug :

          • 看門狗不生效的 BUG。
          • 看門狗導(dǎo)致死鎖的 BUG。

          為了能讓你絲滑入戲,我還是先簡單的給你鋪墊一下,Redisson 的看門狗到底是個(gè)啥東西。

          看門狗描述

          你去看 Redisson 的 wiki 文檔,在鎖的這一部分,開篇就提到了一個(gè)單詞:watchdog

          https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

          watchdog,就是看門狗的意思。

          它是干啥用的呢?

          好的,如果你回答不上來這個(gè)問題。那當(dāng)你遇到下面這個(gè)面試題的時(shí)候肯定懵逼。

          面試官:請問你用 Redis 做分布式鎖的時(shí)候,如果指定過期時(shí)間到了,把鎖給釋放了。但是任務(wù)還未執(zhí)行完成,導(dǎo)致任務(wù)再次被執(zhí)行,這種情況你會(huì)怎么處理呢?

          這個(gè)時(shí)候,99% 的面試官想得到的回答都是看門狗,或者一種類似于看門狗的機(jī)制。

          如果你說:這個(gè)問題我遇到過,但是我就是把過期時(shí)間設(shè)置的長一點(diǎn)。

          時(shí)間到底設(shè)置多長,是你一個(gè)非常主觀的判斷,設(shè)置的長一點(diǎn),能一定程度上解決這個(gè)問題,但是不能完全解決。

          所以,請回去等通知吧。

          或者你回答:這個(gè)問題我遇到過,我不設(shè)置過期時(shí)間,由程序調(diào)用 unlock 來保證。

          好的,程序保證調(diào)用 unlock 方法沒毛病,這是在程序?qū)用婵煽?、可保證的。但是如果你程序運(yùn)行的服務(wù)器剛好還沒來得及執(zhí)行 unlock 就宕機(jī)了呢,這個(gè)你不能打包票吧?

          這個(gè)鎖是不是就死鎖了?

          所以......

          為了解決前面提到的過期時(shí)間不好設(shè)置,以及一不小心死鎖的問題,Redisson 內(nèi)部基于時(shí)間輪,針對每一個(gè)鎖都搞了一個(gè)定時(shí)任務(wù),這個(gè)定時(shí)任務(wù),就是看門狗。

          在 Redisson 實(shí)例被關(guān)閉前,這個(gè)狗子可以通過定時(shí)任務(wù)不斷的延長鎖的有效期。

          因?yàn)槟愀揪筒恍枰O(shè)置過期時(shí)間,這樣就從根本上解決了“過期時(shí)間不好設(shè)置”的問題。默認(rèn)情況下,看門狗的檢查鎖的超時(shí)時(shí)間是 30 秒鐘,也可以通過修改參數(shù)來另行指定。

          如果很不幸,節(jié)點(diǎn)宕機(jī)了導(dǎo)致沒有執(zhí)行 unlock,那么在默認(rèn)的配置下最長 30s 的時(shí)間后,這個(gè)鎖就自動(dòng)釋放了。

          那么問題來了,面試官緊接著來一個(gè)追問:怎么自動(dòng)釋放呢?

          這個(gè)時(shí)候,你只需要來一個(gè)戰(zhàn)術(shù)后仰:程序都沒了,你覺得定時(shí)任務(wù)還在嗎?定時(shí)任務(wù)都不在了,所以也不會(huì)存在死鎖的問題。

          搞 Demo

          前面簡單介紹了原理,我也還是給你搞個(gè)簡單的 Demo 跑一把,這樣更加的直觀。

          引入依賴,啟動(dòng) Redis 什么的就不說了,直接看代碼。

          示例代碼非常簡單,就這么一點(diǎn)內(nèi)容,非常常規(guī)的使用方法:

          把項(xiàng)目啟動(dòng)起來,觸發(fā)接口之后,通過工具觀察 Redis 里面 whyLock 這個(gè) key 的情況,是這樣的:

          你可以看到在我的截圖里面,是有過期時(shí)間的,也就是我打箭頭的地方。

          然后我給你搞個(gè)動(dòng)圖,你仔細(xì)看過期時(shí)間(TTL)這個(gè)地方,有一個(gè)從 20s 變回 30s 的過程:

          首先,我們的代碼里面并沒有設(shè)置過期時(shí)間的動(dòng)作,也沒有去更新過期時(shí)間的這個(gè)動(dòng)作。

          那么這個(gè)東西是怎么回事呢?

          很簡單,Redisson 幫我們做了這些事情,開箱即用,當(dāng)個(gè)黑盒就完事了。

          接下來我就是帶你把黑盒變成白盒,然后引出前面提到的兩個(gè) bug。

          我的測試用例里面用的是 3.16.0 版本的 Redission,我們先找一下它關(guān)于設(shè)置過期動(dòng)作的源碼。

          首先可以看到,我雖然調(diào)用的是無參的 lock 方法,但是它其實(shí)也只是一層皮而已,里面還是調(diào)用了帶入?yún)⒌?lock 方法,只不過給了幾個(gè)默認(rèn)值,其中 leaseTime 給的是 -1:

          而有參的 lock 的源碼是這樣的,主要把注意力放到我框起來的這一行代碼中:

          tryAcquire 方法是它的核心邏輯,那么這個(gè)方法是在干啥事兒呢?

          點(diǎn)進(jìn)去看看,這部分源碼又是這樣的:

          其中 tryLockInnerAsync 方法就是執(zhí)行 Redis 的 Lua 腳本來加鎖。

          既然是加鎖了,過期時(shí)間肯定就是在這里設(shè)置的,也就是這里的 leaseTime:

          而這里的 leaseTime 是在構(gòu)造方法里面初始化的,在我的 Demo 里面,用的是配置中的默認(rèn)值,也就是 30s :

          所以,為什么我們的代碼里面并沒有設(shè)置過期時(shí)間的動(dòng)作,但是對應(yīng)的 key 卻有過期時(shí)間呢?

          這里的源碼回答了這個(gè)問題。

          額外提一句,這個(gè)時(shí)間是從配置中獲取的,所以肯定是可以自定義的,不一定非得是 30s。

          另外需要注意的是,到這里,我們出現(xiàn)了兩個(gè)不同的 leaseTime。

          分別是這樣的:

          • tryAcquireOnceAsync 方法的入?yún)?leaseTime,我們的示例中是 -1。
          • tryLockInnerAsync 方法的入?yún)?leaseTime,我們的示例中是默認(rèn)值 30 * 1000。

          在前面加完鎖之后,緊接著就輪到看門狗工作了:

          前面我說了,這里的 leaseTime 是 -1,所以觸發(fā)的是 else 分支中的 scheduleExpirationRenewal 代碼。

          而這個(gè)代碼就是啟動(dòng)看門狗的代碼。

          換句話說,如果這里的 leaseTime 不是 -1,那么就不會(huì)啟動(dòng)看門狗。

          那么怎么讓 leaseTime 不是 -1 呢?

          自己指定加鎖時(shí)間:

          說人話就是如果加鎖的時(shí)候指定了過期時(shí)間,那么 Redission 不會(huì)給你開啟看門狗的機(jī)制。

          這個(gè)點(diǎn)是無數(shù)人對看門狗機(jī)制不清楚的人都會(huì)記錯(cuò)的一個(gè)點(diǎn),我曾經(jīng)在一個(gè)群里面據(jù)理力爭,后來被別人拿著源碼一頓亂捶。

          是的,我就是那個(gè)以為指定了過期時(shí)間之后,看門狗還會(huì)繼續(xù)工作的人。

          打臉老疼了,希望你不要步后塵。

          接著來看一下 scheduleExpirationRenewal 的代碼:

          里面就是把當(dāng)前線程封裝成了一個(gè)對象,然后維護(hù)到一個(gè) MAP 中。

          這個(gè) MAP 很重要,我先把它放到這里,混個(gè)眼熟,一會(huì)再說它:

          你只要記住這個(gè) MAP 的 key 是當(dāng)前線程,value 是 ExpirationEntry 對象,這個(gè)對象維護(hù)的是當(dāng)前線程的加鎖次數(shù)。

          然后,我們先看 scheduleExpirationRenewal 方法里面,調(diào)用 MAP 的 putIfAbsent 方法后,返回的 oldEntry 不為空的情況。

          這種情況說明是第一次加鎖,會(huì)觸發(fā) renewExpiration 方法,這個(gè)方法里面就是看門狗的核心邏輯。

          而在 scheduleExpirationRenewal 方法里面,不管前面提到的 oldEntry 是否為空,都會(huì)觸發(fā) addThreadId 方法:

          從源碼中可以看出來,這里僅僅對當(dāng)前線程的加鎖次數(shù)進(jìn)行一個(gè)維護(hù)。

          這個(gè)維護(hù)很好理解,因?yàn)橐С宙i的重入嘛,就得記錄到底重入了幾次。

          加鎖一次,次數(shù)加一。解鎖一次,次數(shù)減一。

          接著看 renewExpiration 方法,這就是看門狗的真面目了:

          首先這一坨邏輯主要就是一個(gè)基于時(shí)間輪的定時(shí)任務(wù)。

          標(biāo)號(hào)為 ④ 的地方,就是這個(gè)定時(shí)任務(wù)觸發(fā)的時(shí)間條件:internalLockLeaseTime / 3。

          前面我說了,internalLockLeaseTime 默認(rèn)情況下是 30* 1000,所以這里默認(rèn)就是每 10 秒執(zhí)行一次續(xù)命的任務(wù),這個(gè)從我前面給到的動(dòng)態(tài)里面也可以看出,ttl 的時(shí)間先從 30 變成了 20 ,然后一下又從 20 變成了 30。

          標(biāo)號(hào)為 ①、② 的地方干的是同一件事,就是檢查當(dāng)前線程是否還有效。

          怎么判斷是否有效呢?

          就是看前面提到的 MAP 中是否還有當(dāng)前線程對應(yīng)的 ExpirationEntry 對象。

          沒有,就說明是被 remove 了。

          那么問題就來了,你看源碼的時(shí)候非常自然而然的就應(yīng)該想到這個(gè)問題:什么時(shí)候調(diào)用這個(gè) MAP 的 remove 方法呢?

          很快,在接下來講釋放鎖的地方,你就可以看到對應(yīng)的 remove。這里先提一下,后面就能呼應(yīng)上了。

          核心邏輯是標(biāo)號(hào)為 ③ 的地方。我?guī)阕屑?xì)看看,主要關(guān)注我加了下劃線的地方。

          能走到 ③ 這里說明當(dāng)前線程的業(yè)務(wù)邏輯還未執(zhí)行完成,還需要繼續(xù)持有鎖。

          首先看 renewExpirationAsync 方法,從方法命名上我們也可以看出來,這是在重置過期時(shí)間:

          上面的源碼主要是一個(gè) lua 腳本,而這個(gè)腳本的邏輯非常簡單。就是判斷鎖是否還存在,且持有鎖的線程是否是當(dāng)前線程。如果是當(dāng)前線程,重置鎖的過期時(shí)間,并返回 1,即返回 true。

          如果鎖不存在,或者持有鎖的不是當(dāng)前線程,那么則返回 0,即返回 false。

          接著標(biāo)號(hào)為 ③ 的地方,里面首先判斷了執(zhí)行 renewExpirationAsync 方法是否有異常。

          那么問題就來了,會(huì)有什么異常呢?

          這個(gè)地方的異常,主要是因?yàn)橐?Redis 執(zhí)行命令嘛,所以如果 Redis 出問題了,比如卡住了,或者掉線了,或者連接池沒有連接了等等各種情況,都可能會(huì)執(zhí)行不了命令,導(dǎo)致異常。

          如果出現(xiàn)異常了,則執(zhí)行下面這行代碼:

          EXPIRATION_RENEWAL_MAP.remove(getEntryName());

          然后就 return ,這個(gè)定時(shí)任務(wù)就結(jié)束了。

          好,記住這個(gè) remove 的操作,非常重要,先混個(gè)眼熟,一會(huì)會(huì)講。

          如果執(zhí)行 renewExpirationAsync 方法的時(shí)候沒有異常。這個(gè)時(shí)候的返回值就是 true 或者 false。

          如果是 true,說明續(xù)命成功,則再次調(diào)用 renewExporation 方法,等待著時(shí)間輪觸發(fā)下一次。

          如果是 false,說明這把鎖已經(jīng)沒有了,或者易主了。那么也就沒有當(dāng)前線程什么事情了,啥都不用做,默默的結(jié)束就行了。

          上鎖和看門狗的一些基本原理就是前面說到這么多。

          接著簡單看看 unlock 方法里面是怎么回事兒的。

          首先是 unlockInnerAsync 方法,這里面就是 lua 腳本釋放鎖的邏輯:

          這個(gè)方法返回的是 Boolean,有三種情況。

          • 返回為 null,說明鎖不存在,或者鎖存在,但是 value 不匹配,表示鎖已經(jīng)被其他線程占用。
          • 返回為 true,說明鎖存在,線程也是對的,重入次數(shù)已經(jīng)減為零,鎖可以被釋放。
          • 返回為 false,說明鎖存在,線程也是對的,但是重入次數(shù)還不為零,鎖還不能被釋放。

          但是你看 unlockInnerAsync 是怎么處理這個(gè)返回值的:

          返回值,也就是 opStatus,僅僅是判斷了返回為 null 的情況,拋出異常表明這個(gè)鎖不是被當(dāng)前線程持有的,完事。

          它并不關(guān)心返回為 true 或者為 false 的情況。

          然后再看我框起來的 ?cancelExpirationRenewal(threadId); 方法:

          這里面就有 remove 方法。

          而前面鋪墊了這么多其實(shí)就是為了引出這個(gè) cancelExpirationRenewal 方法。

          縱觀一下加鎖和解鎖,針對 MAP 的操作,看一下下面的這個(gè)圖片:

          標(biāo)號(hào)為 ① 的地方是加鎖,調(diào)用 MAP 的 put 方法。

          標(biāo)號(hào)為 ② 的地方是放鎖,調(diào)用 MAP 的 remove 方法。

          記住上面這一段分析,和操作這個(gè) MAP 的時(shí)機(jī),下面說的 BUG 都是由于對這個(gè) MAP 的操作不恰當(dāng)導(dǎo)致的。

          看門狗不生效的BUG

          前面找了一個(gè)版本給大家看源碼,主要是為了讓大家把 Demo 跑起來,畢竟引入 maven 依賴的成本是小很多的。

          但是真的要研究源碼,還是得把先把源碼拉下來,慢慢的啃起來。

          直接拉項(xiàng)目源碼的好處我在之前的文章里面已經(jīng)說很多次了,對我而言,無外乎就三個(gè)目的:

          • 可以保證是最新的源碼
          • 可以看到代碼的提交記錄
          • 可以找到官方的測試用例

          好,話不多說,首先我們看看開篇說的第一個(gè) BUG:看門狗不生效的問題。

          從這個(gè) issues 說起:

          https://github.com/redisson/redisson/issues/2515

          在這個(gè) issues 里面,他給到了一段代碼,然后說他預(yù)期的結(jié)果是在看門狗續(xù)命期間,如果出現(xiàn)程序和 Redis 的連接問題,導(dǎo)致鎖自動(dòng)過期了,那么我再次申請同一把鎖,應(yīng)該是讓看門狗再次工作才對。

          但是實(shí)際的情況是,即使前一把鎖由于連接異常導(dǎo)致過期了,程序再成功申請到一把新鎖,但是這個(gè)新的鎖,30s 后就自動(dòng)過期了,即看門狗不會(huì)工作。

          這個(gè) issues 對應(yīng)的 pr 是這個(gè):

          https://github.com/redisson/redisson/pull/2518

          在這個(gè) pr 里面,提供了一個(gè)測試用例,我們可以直接在源碼里面找到:

          org.redisson.RedissonLockExpirationRenewalTest

          這就是拉源碼的好處。

          在這個(gè)測試用例里面,核心邏輯是這樣的:

          首先需要說明的是,在這個(gè)測試用例里面,把看門狗的 lockWatchdogTimeout 參數(shù)修改為 1000 ms:

          也就是說看門狗這個(gè)定時(shí)任務(wù),每 333ms 就會(huì)觸發(fā)一次。

          然后我們看標(biāo)號(hào)為 ① 的地方,先申請了一把鎖,然后 Redis 發(fā)生了一次重啟,重啟導(dǎo)致這把鎖失效了,比如還沒來得及持久化,或者持久化了,但是重啟的時(shí)間超過了 1s,這鎖就沒了。

          所以,在調(diào)用 unlock 方法的時(shí)候,肯定會(huì)拋出 IllegalMonitorStateException 異常,表示這把鎖沒了。

          到這里一切正常,還能理解。

          但是看標(biāo)號(hào)為 ② 的地方。

          加鎖之后,業(yè)務(wù)邏輯會(huì)執(zhí)行 2s,肯定會(huì)觸發(fā)看門狗續(xù)命的操作。

          在這個(gè) bug 修復(fù)之前,在這里調(diào)用 unlock 方法也會(huì)拋出 IllegalMonitorStateException 異常,表示這把鎖沒了:

          先不說為啥吧,至少這妥妥的是一個(gè) Bug 了。

          因?yàn)榘凑照5倪壿?,這個(gè)鎖應(yīng)該一直被續(xù)命,然后直到調(diào)用 unlock 才應(yīng)該被釋放。

          好,bug 的演示你也看到了,也可以復(fù)現(xiàn)了。你猜是什么原因?

          答案其實(shí)我在前面應(yīng)該給你寫出來了,就看這波前后呼應(yīng)你能不能反應(yīng)過來了。

          首先前提是兩次加鎖的線程是同一個(gè),然后我前面不是特意強(qiáng)調(diào)了 oldEntry 這個(gè)玩意嗎:

          上面這個(gè) bug 能出現(xiàn),說明第二次 lock 的時(shí)候 oldEntry 在 MAP 里面是存在的,因此誤以為當(dāng)前看門狗正在工作,直接進(jìn)入重入鎖的邏輯即可。

          為什么第二次 lock 的時(shí)候 oldEntry 在 MAP 里面是存在的呢?

          因?yàn)榈谝淮?unlock 的時(shí)候,沒有從 MAP 里面把當(dāng)前線程的 ExpirationEntry 對象移走。

          為什么沒有移走呢?

          看一下這個(gè)哥們測試的 Redisson 版本:

          在這個(gè)版本里面,釋放鎖的邏輯是這樣的:

          誒,不對呀,這不是有 cancelExpirationRenewal(threadId) 的邏輯嗎?

          沒錯(cuò),確實(shí)有。

          但是你看什么情況下會(huì)執(zhí)行這個(gè)邏輯。

          首先是出現(xiàn)異常的情況,但是在我們的測試用例中,兩次調(diào)用 unlock 的時(shí)候 Redis 是正常的,不會(huì)拋出異常。

          然后是 opStatus 不為 null 的時(shí)候會(huì)執(zhí)行該邏輯。

          也就是說 opStatus 為 null 的時(shí)候,即當(dāng)前鎖沒有了,或者易主了的時(shí)候,不會(huì)觸發(fā) cancelExpirationRenewal(threadId) 的邏輯。

          巧了,在我們的場景里面,第一次調(diào)用 unlock 方法的時(shí)候,就是因?yàn)?Redis 重啟導(dǎo)致鎖沒有了,因此這里返回的 opStatus 為 null,沒有觸發(fā) cancelExpirationRenewal 方法的邏輯。

          導(dǎo)致我第二次在當(dāng)前線程中調(diào)用 lock 的時(shí)候,走到下面這里的時(shí)候,oldEntry 不為空:

          所以,走了重入的邏輯,并沒有啟動(dòng)看門狗。

          由于沒有啟動(dòng)看門狗,導(dǎo)致這個(gè)鎖在 1000ms 之后就自動(dòng)釋放了,可以被別的線程搶走拿去用。

          隨后當(dāng)前線程業(yè)務(wù)邏輯執(zhí)行完成,第二次調(diào)用 unlock,當(dāng)然就會(huì)拋出異常了。

          這就是 BUG 的根因。

          找到問題就好了,一行代碼就能解決:

          只要調(diào)用了 unlock 方法,不管怎么樣,先調(diào)用 cancelExpirationRenewal(threadId) 方法,準(zhǔn)沒錯(cuò)。

          這就是由于沒有及時(shí)從 MAP 里面移走當(dāng)前線程對應(yīng)的對象,導(dǎo)致的一個(gè) BUG。

          再看看另外一個(gè)的 issue:

          https://github.com/redisson/redisson/issues/3714

          這個(gè)問題是說如果我的鎖由于某些原因沒了,當(dāng)我在程序里面再次獲取到它之后,看門狗應(yīng)該繼續(xù)工作。

          聽起來,說的是同一個(gè)問題對不對?

          是的,就是說的同一個(gè)問題。

          但是這個(gè)問題,提交的代碼是這樣的:

          在看門狗這里,如果看門狗續(xù)命失敗,說明鎖不存在了,即 res 返回為 false,那么也主動(dòng)執(zhí)行一下 cancelExpirationRenewal 方法,方便為后面的加鎖成功的線程讓路,以免耽誤別人開啟看門狗機(jī)制。

          這樣就能有雙重保障了,在 unlock 和看門狗里面都會(huì)觸發(fā) cancelExpirationRenewal 的邏輯,而且這兩個(gè)邏輯也并不會(huì)沖突。

          另外,我提醒一下,最終提交的代碼是這樣的,兩個(gè)方法入?yún)⑹遣灰粯拥模?/p>

          為什么從 threadId 修改為 null 呢?

          留個(gè)思考題吧,就是從重入的角度考慮的,可以自己去研究一下,很簡單的。

          看門狗導(dǎo)致死鎖的BUG

          這個(gè) BUG 解釋起來就很簡單了。

          看看這個(gè) issue:

          https://github.com/redisson/redisson/issues/1966

          在這里把復(fù)現(xiàn)的步驟都寫的清清楚楚的。

          測試程序是這樣的,通過定時(shí)任務(wù) 1s 觸發(fā)一次,但是任務(wù)會(huì)執(zhí)行 2s,這樣就會(huì)導(dǎo)致鎖的重入:

          他這里提到一個(gè)命令:

          CLIENT PAUSE 5000

          主要還是模擬 Redis 處理請求超時(shí)的情況,就是讓 Redis 假死 5s,這樣程序發(fā)過來的請求就會(huì)超時(shí)。

          這樣,重入的邏輯就會(huì)發(fā)生混亂。

          看一下這個(gè) bug 修復(fù)的對應(yīng)的關(guān)鍵代碼之一:

          不管 opStatus 返回為 false 還是 true,都執(zhí)行 cancelExpirationRenewal 邏輯。

          問題的解決之道,還是在于對 MAP 的操作。

          另外,多提一句。

          也是在這次提交中,把維護(hù)重入的邏輯封裝到了 ExpirationEntry 這個(gè)對象里面,比起之前的寫法優(yōu)雅了很多,有興趣的可以把源碼拉下來進(jìn)行一下對比,感受一下什么叫做優(yōu)雅的重構(gòu):

          線程中斷

          在寫文章的時(shí)候,我還發(fā)現(xiàn)一個(gè)有意思的,但對于 Redisson 無解的 bug。

          就是這里:

          我第一眼看到這一段代碼就很奇怪,這樣奇怪的寫法,背后肯定是有故事的。

          這背后對應(yīng)的故事,藏在這個(gè) issue 里面:

          https://github.com/redisson/redisson/issues/2714

          翻譯過來,說的是當(dāng) tryLock 方法被中斷時(shí),看門狗還是會(huì)不斷地更新鎖,這就造成了無限鎖,也就是死鎖。

          我們看一下對應(yīng)的測試用例:

          開啟了一個(gè)子線程,在子線程里面執(zhí)行了 tryLock 的方法,然后主線程里面調(diào)用了子線程的 interrupt 方法。

          你說這個(gè)時(shí)候子線程應(yīng)該怎么辦?

          按理來說,線程被中斷了,是不是看門狗也不應(yīng)該工作呢?

          是的,所以這樣的代碼就出現(xiàn)了:

          但是,你細(xì)品,這幾行代碼并沒有完全解決看門狗的問題。只能在一定概率上解決第一次調(diào)用后 renewExpiration 方法后,還沒來得及啟動(dòng)定時(shí)任務(wù)之前的這一小段時(shí)間。

          所以,測試案例里面的 sleep 時(shí)間,只有 5ms:

          這時(shí)間要是再長一點(diǎn),就會(huì)觸發(fā)看門狗機(jī)制。

          一旦觸發(fā)看門狗機(jī)制,觸發(fā) renewExpiration 方法的線程就會(huì)變成定時(shí)任務(wù)的線程。

          你外面的子線程 interrupt 了,和我定時(shí)任務(wù)的線程有什么關(guān)系?

          比如,我把這幾行代碼移動(dòng)到這里:

          其實(shí)沒有任何卵用:

          因?yàn)榫€程變了。

          對于這個(gè)問題,官方的回答是這樣的:

          大概意思就是說:嗯,你說的很有道理,但是 Redisson 的看門狗工作范圍是整個(gè)實(shí)例,而不是某個(gè)指定的線程。

          意外收獲

          最后,再來一個(gè)意外收獲:

          你看 addThreadId 這個(gè)方法重構(gòu)了一次。

          但是這次重構(gòu)就出現(xiàn)問題了。

          原來的邏輯是當(dāng) counter 是 null 的時(shí)候,初始化為 1。不為 null 的時(shí)候,就執(zhí)行 counter++,即重入。

          重構(gòu)之后的邏輯是當(dāng) counter 是 null 的時(shí)候,先初始化為 1,然后緊接著執(zhí)行 counter++。

          那豈不是 counter 直接就變成了 2,和原來的邏輯不一樣了?

          是的,不一樣了。

          搞的我 Debug 的時(shí)候一臉懵逼,后來才發(fā)現(xiàn)這個(gè)地方出現(xiàn)問題了。

          那就不好意思了,意外收獲,混個(gè) pr 吧:

          荒腔走板

          這期沒啥好荒的了,我在《五一躺平的感覺怎么說呢?太特么爽了!》這篇推文里面都已經(jīng)荒過了,沒看過的可以去看看我沒有學(xué)習(xí)的、五一的快樂生活。

          雖然快樂吧,但是我感覺自己還沒休息好呢,五一唰的一下就沒了。

          還沒開始上班,我就已經(jīng)隱隱感覺到有點(diǎn)累了。

          因?yàn)?8 號(hào)放假的那天剛好輪到我值班,你可以理解為那天需要我加班,你知道這意味著啥嗎?

          這意味著我得連續(xù)上 9 天班。

          你說遭不遭得???

          遭不住,對不對。

          所以,即使五一已經(jīng)過去了,我還是想向發(fā)明五一調(diào)休的人才問好:我衷心的謝謝你全家。


          瀏覽 25
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色一级看 | 五月丁香婷婷激情综合 | 操逼资源| 好男人一区二区三区在线观看 | 亚州精品久久久久久久久 |