精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Redis 分布式鎖的正確實現原理演化歷程與 Redisson 實戰總結

存儲 存儲軟件 分布式 Redis
Redis 分布式鎖使用 SET 指令就可以實現了么?在分布式領域 CAP 理論一直存在。分布式鎖的門道可沒那么簡單,我們在網上看到的分布式鎖方案可能是有問題的。

[[437124]]

Redis 分布式鎖使用 SET 指令就可以實現了么?在分布式領域 CAP 理論一直存在。

分布式鎖的門道可沒那么簡單,我們在網上看到的分布式鎖方案可能是有問題的。

「碼哥」一步步帶你深入分布式鎖是如何一步步完善,在高并發生產環境中如何正確使用分布式鎖。

在進入正文之前,我們先帶著問題去思考:

  • 什么時候需要分布式鎖?
  • 加、解鎖的代碼位置有講究么?
  • 如何避免出現鎖再也無法刪除?
  • 超時時間設置多少合適呢?
  • 如何避免鎖被其他線程釋放
  • 如何實現重入鎖?
  • 主從架構會帶來什么安全問題?
  • 什么是 Redlock
  • Redisson 分布式鎖最佳實戰
  • 看門狗實現原理
  • ……

什么時候用分布式鎖?

碼哥,說個通俗的例子講解下什么時候需要分布式鎖呢?

診所只有一個醫生,很多患者前來就診。

醫生在同一時刻只能給一個患者提供就診服務。

如果不是這樣的話,就會出現醫生在就診腎虧的「肖菜雞」準備開藥時候患者切換成了腳臭的「謝霸哥」,這時候藥就被謝霸哥取走了。

治腎虧的藥被有腳臭的拿去了。

當并發去讀寫一個【共享資源】的時候,我們為了保證數據的正確,需要控制同一時刻只有一個線程訪問。

分布式鎖就是用來控制同一時刻,只有一個 JVM 進程中的一個線程可以訪問被保護的資源。

分布式鎖入門

65 哥:分布式鎖應該滿足哪些特性?

互斥:在任何給定時刻,只有一個客戶端可以持有鎖;

無死鎖:任何時刻都有可能獲得鎖,即使獲取鎖的客戶端崩潰;

容錯:只要大多數 Redis的節點都已經啟動,客戶端就可以獲取和釋放鎖。

碼哥,我可以使用 SETNX key value 命令是實現「互斥」特性。

這個命令來自于SET if Not eXists的縮寫,意思是:如果 key 不存在,則設置 value 給這個key,否則啥都不做。Redis 官方地址說的:

命令的返回值:

  • 1:設置成功;
  • 0:key 沒有設置成功。

如下場景:

敲代碼一天累了,想去放松按摩下肩頸。

168 號技師最搶手,大家喜歡點,所以并發量大,需要分布式鎖控制。

同一時刻只允許一個「客戶」預約 168 技師。

肖菜雞申請 168 技師成功:

  1. > SETNX lock:168 1 
  2. (integer) 1 # 獲取 168 技師成功 

謝霸哥后面到,申請失敗:

  1. > SETNX lock 2 
  2.  
  3. (integer) 0 # 客戶謝霸哥 2 獲取失敗 

此刻,申請成功的客戶就可以享受 168 技師的肩頸放松服務「共享資源」。

享受結束后,要及時釋放鎖,給后來者享受 168 技師的服務機會。

肖菜雞,碼哥考考你如何釋放鎖呢?

很簡單,使用 DEL 刪除這個 key 就行。

  1. > DEL lock:168 
  2. (integer) 1 

碼哥,你見過「龍」么?我見過,因為我被一條龍服務過。

肖菜雞,事情可沒這么簡單。

這個方案存在一個存在造成鎖無法釋放的問題,造成該問題的場景如下:

客戶端所在節點崩潰,無法正確釋放鎖;

業務邏輯異常,無法執行 DEL指令。

這樣,這個鎖就會一直占用,鎖在我手里,我掛了,這樣其他客戶端再也拿不到這個鎖了。

超時設置

碼哥,我可以在獲取鎖成功的時候設置一個「超時時間」

比如設定按摩服務一次 60 分鐘,那么在給這個 key 加鎖的時候設置 60 分鐘過期即可:

  1. > SETNX lock:168 1 // 獲取鎖 
  2.  
  3. (integer) 1 
  4.  
  5. > EXPIRE lock:168 60 // 60s 自動刪除 
  6.  
  7. (integer) 1 

這樣,到點后鎖自動釋放,其他客戶就可以繼續享受 168 技師按摩服務了。

誰要這么寫,就糟透了。

「加鎖」、「設置超時」是兩個命令,他們不是原子操作。

如果出現只執行了第一條,第二條沒機會執行就會出現「超時時間」設置失敗,依然出現鎖無法釋放。

碼哥,那咋辦,我想被一條龍服務,要解決這個問題

Redis 2.6.X 之后,官方拓展了 SET 命令的參數,滿足了當 key 不存在則設置 value,同時設置超時時間的語義,并且滿足原子性。

  1. SET resource_name random_value NX PX 30000 
  • NX:表示只有 resource_name 不存在的時候才能 SET 成功,從而保證只有一個客戶端可以獲得鎖;
  • PX 30000:表示這個鎖有一個 30 秒自動過期時間。

這樣寫還不夠,我們還要防止不能釋放不是自己加的鎖。我們可以在 value 上做文章。

繼續往下看……

釋放了不是自己加的鎖

這樣我能穩妥的享受一條龍服務了么?

No,還有一種場景會導致釋放別人的鎖:

客戶 1 獲取鎖成功并設置設置 30 秒超時;

客戶 1 因為一些原因導致執行很慢(網絡問題、發生 FullGC……),過了 30 秒依然沒執行完,但是鎖過期「自動釋放了」;

客戶 2 申請加鎖成功;

客戶 1 執行完成,執行 DEL 釋放鎖指令,這個時候就把客戶 2 的鎖給釋放了。

有個關鍵問題需要解決:自己的鎖只能自己來釋放。

我要如何刪除是自己加的鎖呢?

在執行 DEL 指令的時候,我們要想辦法檢查下這個鎖是不是自己加的鎖再執行刪除指令。

解鈴還須系鈴人

碼哥,我在加鎖的時候設置一個「唯一標識」作為 value 代表加鎖的客戶端。SET resource_name random_value NX PX 30000

在釋放鎖的時候,客戶端將自己的「唯一標識」與鎖上的「標識」比較是否相等,匹配上則刪除,否則沒有權利釋放鎖。

偽代碼如下:

  1. // 比對 value 與 唯一標識 
  2. if (redis.get("lock:168").equals(random_value)){ 
  3.    redis.del("lock:168"); //比對成功則刪除 
  4.  } 

有沒有想過,這是 GET + DEL 指令組合而成的,這里又會涉及到原子性問題。

我們可以通過 Lua 腳本來實現,這樣判斷和刪除的過程就是原子操作了。

  1. // 獲取鎖的 value 與 ARGV[1] 是否匹配,匹配則執行 del 
  2. if redis.call("get",KEYS[1]) == ARGV[1] then 
  3.     return redis.call("del",KEYS[1]) 
  4. else 
  5.     return 0 
  6. end 

這樣通過唯一值設置成 value 標識加鎖的客戶端很重要,僅使用 DEL 是不安全的,因為一個客戶端可能會刪除另一個客戶端的鎖。

使用上面的腳本,每個鎖都用一個隨機字符串“簽名”,只有當刪除鎖的客戶端的“簽名”與鎖的 value 匹配的時候,才會刪除它。

官方文檔也是這么說的:https://redis.io/topics/distlock

這個方案已經相對完美,我們用的最多的可能就是這個方案了。

正確設置鎖超時

鎖的超時時間怎么計算合適呢?

這個時間不能瞎寫,一般要根據在測試環境多次測試,然后壓測多輪之后,比如計算出平均執行時間 200 ms。

那么鎖的超時時間就放大為平均執行時間的 3~5 倍。

為啥要放放大呢?

因為如果鎖的操作邏輯中有網絡 IO 操作、JVM FullGC 等,線上的網絡不會總一帆風順,我們要給網絡抖動留有緩沖時間。

那我設置更大一點,比如設置 1 小時不是更安全?

不要鉆牛角,多大算大?

設置時間過長,一旦發生宕機重啟,就意味著 1 小時內,分布式鎖的服務全部節點不可用。

你要讓運維手動刪除這個鎖么?

只要運維真的不會打你。

有沒有完美的方案呢?不管時間怎么設置都不大合適。

我們可以讓獲得鎖的線程開啟一個守護線程,用來給快要過期的鎖「續航」。

加鎖的時候設置一個過期時間,同時客戶端開啟一個「守護線程」,定時去檢測這個鎖的失效時間。

如果快要過期,但是業務邏輯還沒執行完成,自動對這個鎖進行續期,重新設置過期時間。

這個道理行得通,可我寫不出。

別慌,已經有一個庫把這些工作都封裝好了他叫 Redisson。

在使用分布式鎖時,它就采用了「自動續期」的方案來避免鎖過期,這個守護線程我們一般也把它叫做「看門狗」線程。

一路優化下來,方案似乎比較「嚴謹」了,抽象出對應的模型如下。

  1. 通過 SET lock_resource_name random_value NX PX expire_time,同時啟動守護線程為快要過期但還沒執行完的客戶端的鎖續命;
  2. 客戶端執行業務邏輯操作共享資源;
  3. 通過 Lua 腳本釋放鎖,先 get 判斷鎖是否是自己加的,再執行 DEL。

這個方案實際上已經比較完美,能寫到這一步已經打敗 90% 的程序猿了。

但是對于追求極致的程序員來說還遠遠不夠:

  1. 可重入鎖如何實現?
  2. 主從架構崩潰恢復導致鎖丟失如何解決?
  3. 客戶端加鎖的位置有門道么?

加解鎖代碼位置有講究

根據前面的分析,我們已經有了一個「相對嚴謹」的分布式鎖了。

于是「謝霸哥」就寫了如下代碼將分布式鎖運用到項目中,以下是偽代碼邏輯:

  1. public void doSomething() { 
  2.   redisLock.lock(); // 上鎖 
  3.     try { 
  4.         // 處理業務 
  5.         ..... 
  6.         redisLock.unlock(); // 釋放鎖 
  7.     } catch (Exception e) { 
  8.         e.printStackTrace(); 
  9.     } 

有沒有想過:一旦執行業務邏輯過程中拋出異常,程序就無法執行釋放鎖的流程。

所以釋放鎖的代碼一定要放在 finally{} 塊中。

加鎖的位置也有問題,放在 try 外面的話,如果執行 redisLock.lock() 加鎖異常,但是實際指令已經發送到服務端并執行,只是客戶端讀取響應超時,就會導致沒有機會執行解鎖的代碼。

所以 redisLock.lock() 應該寫在 try 代碼塊,這樣保證一定會執行解鎖邏輯。

綜上所述,正確代碼位置如下 :

  1. public void doSomething() { 
  2.     try { 
  3.         // 上鎖 
  4.         redisLock.lock(); 
  5.         // 處理業務 
  6.         ... 
  7.     } catch (Exception e) { 
  8.         e.printStackTrace(); 
  9.     } finally { 
  10.       // 釋放鎖 
  11.       redisLock.unlock(); 
  12.     } 

實現可重入鎖

65 哥:可重入鎖要如何實現呢?

當一個線程執行一段代碼成功獲取鎖之后,繼續執行時,又遇到加鎖的代碼,可重入性就就保證線程能繼續執行,而不可重入就是需要等待鎖釋放之后,再次獲取鎖成功,才能繼續往下執行。

用一段代碼解釋可重入:

  1. public synchronized void a() { 
  2.     b(); 
  3. public synchronized void b() { 
  4.     // pass 

假設 X 線程在 a 方法獲取鎖之后,繼續執行 b 方法,如果此時不可重入,線程就必須等待鎖釋放,再次爭搶鎖。

鎖明明是被 X 線程擁有,卻還需要等待自己釋放鎖,然后再去搶鎖,這看起來就很奇怪,我釋放我自己~

Redis Hash 可重入鎖

Redisson 類庫就是通過 Redis Hash 來實現可重入鎖

當線程擁有鎖之后,往后再遇到加鎖方法,直接將加鎖次數加 1,然后再執行方法邏輯。

退出加鎖方法之后,加鎖次數再減 1,當加鎖次數為 0 時,鎖才被真正的釋放。

可以看到可重入鎖最大特性就是計數,計算加鎖的次數。

所以當可重入鎖需要在分布式環境實現時,我們也就需要統計加鎖次數。

加鎖邏輯

我們可以使用 Redis hash 結構實現,key 表示被鎖的共享資源, hash 結構的 fieldKey 的 value 則保存加鎖的次數。

通過 Lua 腳本實現原子性,假設 KEYS1 = 「lock」, ARGV「1000,uuid」:

  1. ---- 1 代表 true 
  2. ---- 0 代表 false 
  3. if (redis.call('exists', KEYS[1]) == 0) then 
  4.     redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  5.     redis.call('pexpire', KEYS[1], ARGV[1]); 
  6.     return 1; 
  7. end ; 
  8. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
  9.     redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  10.     redis.call('pexpire', KEYS[1], ARGV[1]); 
  11.     return 1; 
  12. end ; 
  13. return 0; 

加鎖代碼首先使用 Redis exists 命令判斷當前 lock 這個鎖是否存在。

如果鎖不存在的話,直接使用 hincrby創建一個鍵為 lock hash 表,并且為 Hash 表中鍵為 uuid 初始化為 0,然后再次加 1,最后再設置過期時間。

如果當前鎖存在,則使用 hexists判斷當前 lock 對應的 hash 表中是否存在 uuid 這個鍵,如果存在,再次使用 hincrby 加 1,最后再次設置過期時間。

最后如果上述兩個邏輯都不符合,直接返回。

解鎖邏輯

-- 判斷 hash set 可重入 key 的值是否等于 0

-- 如果為 0 代表 該可重入 key 不存在

  1. -- 判斷 hash set 可重入 key 的值是否等于 0 
  2. -- 如果為 0 代表 該可重入 key 不存在 
  3. if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then 
  4.     return nil; 
  5. end ; 
  6. -- 計算當前可重入次數 
  7. local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); 
  8. -- 小于等于 0 代表可以解鎖 
  9. if (counter > 0) then 
  10.     return 0; 
  11. else 
  12.     redis.call('del', KEYS[1]); 
  13.     return 1; 
  14. end ; 
  15. return nil; 

首先使用 hexists 判斷 Redis Hash 表是否存給定的域。

如果 lock 對應 Hash 表不存在,或者 Hash 表不存在 uuid 這個 key,直接返回 nil。

若存在的情況下,代表當前鎖被其持有,首先使用 hincrby使可重入次數減 1 ,然后判斷計算之后可重入次數,若小于等于 0,則使用 del 刪除這把鎖。

解鎖代碼執行方式與加鎖類似,只不過解鎖的執行結果返回類型使用 Long。這里之所以沒有跟加鎖一樣使用 Boolean ,這是因為解鎖 lua 腳本中,三個返回值含義如下:

  • 1 代表解鎖成功,鎖被釋放
  • 0 代表可重入次數被減 1
  • null 代表其他線程嘗試解鎖,解鎖失敗.

主從架構帶來的問題

碼哥,到這里分布式鎖「很完美了」吧,沒想到分布式鎖這么多門道。

路還很遠,之前分析的場景都是,鎖在「單個」Redis 實例中可能產生的問題,并沒有涉及到 Redis 主從模式導致的問題。

我們通常使用「Cluster 集群」或者「哨兵集群」的模式部署保證高可用。

這兩個模式都是基于「主從架構數據同步復制」實現的數據同步,而 Redis 的主從復制默認是異步的。

以下內容來自于官方文檔 https://redis.io/topics/distlock

我們試想下如下場景會發生什么問題:

客戶端 A 在 master 節點獲取鎖成功。

還沒有把獲取鎖的信息同步到 slave 的時候,master 宕機。

slave 被選舉為新 master,這時候沒有客戶端 A 獲取鎖的數據。

客戶端 B 就能成功的獲得客戶端 A 持有的鎖,違背了分布式鎖定義的互斥。

雖然這個概率極低,但是我們必須得承認這個風險的存在。

Redis 的作者提出了一種解決方案,叫 Redlock(紅鎖)

Redis 的作者為了統一分布式鎖的標準,搞了一個 Redlock,算是 Redis 官方對于實現分布式鎖的指導規范,https://redis.io/topics/distlock,但是這個 Redlock 也被國外的一些分布式專家給噴了。

因為它也不完美,有“漏洞”。

什么是 Redlock

紅鎖是不是這個?

[[437127]]

泡面吃多了你,Redlock 紅鎖是為了解決主從架構中當出現主從切換導致多個客戶端持有同一個鎖而提出的一種算法。

大家可以看官方文檔(https://redis.io/topics/distlock),以下來自官方文檔的翻譯。

想用使用 Redlock,官方建議在不同機器上部署 5 個 Redis 主節點,節點都是完全獨立,也不使用主從復制,使用多個節點是為容錯。

一個客戶端要獲取鎖有 5 個步驟:

客戶端獲取當前時間 T1(毫秒級別);

使用相同的 key和 value順序嘗試從 N個 Redis實例上獲取鎖。

每個請求都設置一個超時時間(毫秒級別),該超時時間要遠小于鎖的有效時間,這樣便于快速嘗試與下一個實例發送請求。

比如鎖的自動釋放時間 10s,則請求的超時時間可以設置 5~50 毫秒內,這樣可以防止客戶端長時間阻塞。

客戶端獲取當前時間 T2 并減去步驟 1 的 T1 來計算出獲取鎖所用的時間(T3 = T2 -T1)。當且僅當客戶端在大多數實例(N/2 + 1)獲取成功,且獲取鎖所用的總時間 T3 小于鎖的有效時間,才認為加鎖成功,否則加鎖失敗。

如果第 3 步加鎖成功,則執行業務邏輯操作共享資源,key 的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟 3 計算的結果)。

如果因為某些原因,獲取鎖失敗(沒有在至少 N/2+1 個 Redis 實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的 Redis 實例上進行解鎖(即便某些 Redis 實例根本就沒有加鎖成功)。

另外部署實例的數量要求是奇數,為了能很好的滿足過半原則,如果是 6 臺則需要 4 臺獲取鎖成功才能認為成功,所以奇數更合理

事情可沒這么簡單,Redis 作者把這個方案提出后,受到了業界著名的分布式系統專家的質疑。

兩人好比神仙打架,兩人一來一回論據充足的對一個問題提出很多論斷……

Martin Kleppmann 提出質疑的博客:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Redlock 設計者的回復:http://antirez.com/news/101

Redlock 是與非

Martin Kleppmann 認為鎖定的目的是為了保護對共享資源的讀寫,而分布式鎖應該「高效」和「正確」。

高效性:分布式鎖應該要滿足高效的性能,Redlock 算法向 5 個節點執行獲取鎖的邏輯性能不高,成本增加,復雜度也高;

正確性:分布式鎖應該防止并發進程在同一時刻只能有一個線程能對共享數據讀寫。

出于這兩點,我們沒必要承擔 Redlock 的成本和復雜,運行 5 個 Redis 實例并判斷加鎖是否滿足大多數才算成功。

主從架構崩潰恢復極小可能發生,這沒什么大不了的。使用單機版就夠了,Redlock 太重了,沒必要。

Martin 認為 Redlock 根本達不到安全性的要求,也依舊存在鎖失效的問題!

Martin 的結論

Redlock 不倫不類:對于偏好效率來講,Redlock 比較重,沒必要這么做,而對于偏好正確性來說,Redlock 是不夠安全的。

時鐘假設不合理:該算法對系統時鐘做出了危險的假設(假設多個節點機器時鐘都是一致的),如果不滿足這些假設,鎖就會失效。

無法保證正確性:Redlock 不能提供類似 fencing token 的方案,所以解決不了正確性的問題。為了正確性,請使用有「共識系統」的軟件,例如 Zookeeper。

Redis 作者 Antirez 的反駁

在 Redis 作者的反駁文章中,有 3 個重點:

時鐘問題:Redlock 并不需要完全一致的時鐘,只需要大體一致就可以了,允許有「誤差」,只要誤差不要超過鎖的租期即可,這種對于時鐘的精度要求并不是很高,而且這也符合現實環境。

網絡延遲、進程暫停問題:

客戶端在拿到鎖之前,無論經歷什么耗時長問題,Redlock 都能夠在第 3 步檢測出來

客戶端在拿到鎖之后,發生 NPC,那 Redlock、Zookeeper 都無能為力

質疑 fencing token 機制。

關于 Redlock 的爭論我們下期再見,現在進入 Redisson 實現分布式鎖實戰部分。

Redisson 分布式鎖

基于 SpringBoot starter 方式,添加 starter。

  1. <dependency> 
  2.   <groupId>org.redisson</groupId> 
  3.   <artifactId>redisson-spring-boot-starter</artifactId> 
  4.   <version>3.16.4</version> 
  5. </dependency> 

不過這里需要注意 springboot 與 redisson 的版本,因為官方推薦 redisson 版本與 springboot 版本配合使用。

將 Redisson 與 Spring Boot 庫集成,還取決于 Spring Data Redis 模塊。

「碼哥」使用 SpringBoot 2.5.x 版本, 所以需要添加 redisson-spring-data-25。

  1. <dependency> 
  2.   <groupId>org.redisson</groupId> 
  3.   <!-- for Spring Data Redis v.2.5.x --> 
  4.   <artifactId>redisson-spring-data-25</artifactId> 
  5.   <version>3.16.4</version> 
  6. </dependency> 

添加配置文件

  1. spring: 
  2.   redis: 
  3.     database
  4.     host: 
  5.     port: 
  6.     password
  7.     ssl: 
  8.     timeout: 
  9.     # 根據實際情況配置 cluster 或者哨兵 
  10.     cluster: 
  11.       nodes: 
  12.     sentinel: 
  13.       master: 
  14.       nodes: 

就這樣在 Spring 容器中我們擁有以下幾個 Bean 可以使用:

  • RedissonClient
  • RedissonRxClient
  • RedissonReactiveClient
  • RedisTemplate
  • ReactiveRedisTemplate

失敗無限重試

  1. RLock lock = redisson.getLock("碼哥字節"); 
  2. try { 
  3.  
  4.   // 1.最常用的第一種寫法 
  5.   lock.lock(); 
  6.  
  7.   // 執行業務邏輯 
  8.   ..... 
  9.  
  10. } finally { 
  11.   lock.unlock(); 

拿鎖失敗時會不停的重試,具有 Watch Dog 自動延期機制,默認續 30s 每隔 30/3=10 秒續到 30s。

失敗超時重試,自動續命

  1. // 嘗試拿鎖10s后停止重試,獲取失敗返回false,具有Watch Dog 自動延期機制, 默認續30s 
  2. boolean flag = lock.tryLock(10, TimeUnit.SECONDS); 

超時自動釋放鎖

  1. // 沒有Watch Dog ,10s后自動釋放,不需要調用 unlock 釋放鎖。 
  2.  
  3. lock.lock(10, TimeUnit.SECONDS); 

超時重試,自動解鎖

  1. // 嘗試加鎖,最多等待100秒,上鎖以后10秒自動解鎖,沒有 Watch dog 
  2. boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 
  3. if (res) { 
  4.    try { 
  5.      ... 
  6.    } finally { 
  7.        lock.unlock(); 
  8.    } 

Watch Dog 自動延時

如果獲取分布式鎖的節點宕機,且這個鎖還處于鎖定狀態,就會出現死鎖。

為了避免這個情況,我們都會給鎖設置一個超時自動釋放時間。

然而,還是會存在一個問題。

假設線程獲取鎖成功,并設置了 30 s 超時,但是在 30s 內任務還沒執行完,鎖超時釋放了,就會導致其他線程獲取不該獲取的鎖。

所以,Redisson 提供了 watch dog 自動延時機制,提供了一個監控鎖的看門狗,它的作用是在 Redisson 實例被關閉前,不斷的延長鎖的有效期。

也就是說,如果一個拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放。

默認情況下,看門狗的續期時間是 30s,也可以通過修改 Config.lockWatchdogTimeout 來另行指定。

另外 Redisson 還提供了可以指定 leaseTime 參數的加鎖方法來指定加鎖的時間。

超過這個時間后鎖便自動解開了,不會延長鎖的有效期。

原理如下圖:

有兩個點需要注意:

  • watchDog 只有在未顯示指定加鎖超時時間(leaseTime)時才會生效。
  • lockWatchdogTimeout 設定的時間不要太小 ,比如設置的是 100 毫秒,由于網絡直接導致加鎖完后,watchdog 去延期時,這個 key 在 redis 中已經被刪除了。

源碼導讀

在調用 lock 方法時,會最終調用到 tryAcquireAsync。

調用鏈為:lock()->tryAcquire->tryAcquireAsync,詳細解釋如下:

  1. private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { 
  2.         RFuture<Long> ttlRemainingFuture; 
  3.         //如果指定了加鎖時間,會直接去加鎖 
  4.         if (leaseTime != -1) { 
  5.             ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG); 
  6.         } else { 
  7.             //沒有指定加鎖時間 會先進行加鎖,并且默認時間就是 LockWatchdogTimeout的時間 
  8.             //這個是異步操作 返回RFuture 類似netty中的future 
  9.             ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, 
  10.                     TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); 
  11.         } 
  12.  
  13.         //這里也是類似netty Future 的addListener,在future內容執行完成后執行 
  14.         ttlRemainingFuture.onComplete((ttlRemaining, e) -> { 
  15.             if (e != null) { 
  16.                 return
  17.             } 
  18.  
  19.             // lock acquired 
  20.             if (ttlRemaining == null) { 
  21.                 // leaseTime不為-1時,不會自動延期 
  22.                 if (leaseTime != -1) { 
  23.                     internalLockLeaseTime = unit.toMillis(leaseTime); 
  24.                 } else { 
  25.                     //這里是定時執行 當前鎖自動延期的動作,leaseTime為-1時,才會自動延期 
  26.                     scheduleExpirationRenewal(threadId); 
  27.                 } 
  28.             } 
  29.         }); 
  30.         return ttlRemainingFuture; 
  31.     } 

scheduleExpirationRenewal 中會調用 renewExpiration 啟用了一個 timeout 定時,去執行延期動作。

  1. private void renewExpiration() { 
  2.         ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); 
  3.         if (ee == null) { 
  4.             return
  5.         } 
  6.  
  7.         Timeout task = commandExecutor.getConnectionManager() 
  8.           .newTimeout(new TimerTask() { 
  9.             @Override 
  10.             public void run(Timeout timeout) throws Exception { 
  11.                 // 省略部分代碼 
  12.                 .... 
  13.  
  14.                 RFuture<Boolean> future = renewExpirationAsync(threadId); 
  15.                 future.onComplete((res, e) -> { 
  16.                     .... 
  17.  
  18.                     if (res) { 
  19.                         //如果 沒有報錯,就再次定時延期 
  20.                         // reschedule itself 
  21.                         renewExpiration(); 
  22.                     } else { 
  23.                         cancelExpirationRenewal(null); 
  24.                     } 
  25.                 }); 
  26.             } 
  27.             // 這里我們可以看到定時任務 是 lockWatchdogTimeout 的1/3時間去執行 renewExpirationAsync 
  28.         }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); 
  29.  
  30.         ee.setTimeout(task); 
  31.     } 

scheduleExpirationRenewal 會調用到 renewExpirationAsync,執行下面這段 lua 腳本。

他主要判斷就是 這個鎖是否在 redis 中存在,如果存在就進行 pexpire 延期。

  1. protected RFuture<Boolean> renewExpirationAsync(long threadId) { 
  2.         return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
  3.                 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + 
  4.                         "redis.call('pexpire', KEYS[1], ARGV[1]); " + 
  5.                         "return 1; " + 
  6.                         "end; " + 
  7.                         "return 0;"
  8.                 Collections.singletonList(getRawName()), 
  9.                 internalLockLeaseTime, getLockName(threadId)); 
  10.     } 
  • watch dog 在當前節點還存活且任務未完成則每 10 s 給鎖續期 30s。
  • 程序釋放鎖操作時因為異常沒有被執行,那么鎖無法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
  • 要使 watchLog 機制生效 ,lock 時 不要設置 過期時間。
  • watchlog 的延時時間 可以由 lockWatchdogTimeout 指定默認延時時間,但是不要設置太小。
  • watchdog 會每 lockWatchdogTimeout/3 時間,去延時。
  • 通過 lua 腳本實現延遲。

總結

完工,我建議你合上屏幕,自己在腦子里重新過一遍,每一步都在做什么,為什么要做,解決什么問題。

我們一起從頭到尾梳理了一遍 Redis 分布式鎖中的各種門道,其實很多點是不管用什么做分布式鎖都會存在的問題,重要的是思考的過程。

對于系統的設計,每個人的出發點都不一樣,沒有完美的架構,沒有普適的架構,但是在完美和普適能平衡的很好的架構,就是好的架構。

本文轉載自微信公眾號「碼哥字節」,可以通過以下二維碼關注。轉載本文請聯系碼哥字節公眾號。

 

責任編輯:武曉燕 來源: 碼哥字節
相關推薦

2025-03-25 10:29:52

2024-01-24 13:15:00

Redis分布式鎖SpringBoot

2024-11-28 15:11:28

2024-01-02 13:15:00

分布式鎖RedissonRedis

2022-08-04 08:45:50

Redisson分布式鎖工具

2021-09-17 07:51:24

RedissonRedis分布式

2021-02-28 07:49:28

Zookeeper分布式

2023-01-13 07:39:07

2025-07-30 09:34:04

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數據分布式鎖

2019-06-19 15:40:06

分布式鎖RedisJava

2022-06-30 08:04:16

Redis分布式鎖Redisson

2024-11-06 12:29:02

2021-07-06 08:37:29

Redisson分布式

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2023-09-04 08:12:16

分布式鎖Springboot

2021-03-10 09:54:06

Redis分布式

2023-03-01 08:07:51

2024-10-07 10:07:31

點贊
收藏

51CTO技術棧公眾號

污视频在线看网站| 一级黄色小视频| 欧美高清视频看片在线观看| 色一情一伦一子一伦一区| 亚洲欧美日韩精品久久久| 99国产精品欲| 男人的天堂亚洲在线| 色偷偷888欧美精品久久久| 色诱av手机版| 性欧美freehd18| 亚洲午夜电影在线| 先锋在线资源一区二区三区| 丁香六月天婷婷| 美美哒免费高清在线观看视频一区二区 | 亚洲精品日韩成人| 亚洲乱色熟女一区二区三区| 久久婷婷久久| 久久久亚洲国产天美传媒修理工| av永久免费观看| 国产精品久久久久av蜜臀| 在线视频欧美精品| 性一交一乱一伧国产女士spa| av中文字幕在线| 99久久精品国产精品久久| 91在线视频成人| 伊人久久成人网| 亚洲免费网址| 久久久亚洲影院你懂的| 99久久99久久精品国产| 成人3d动漫在线观看| 日韩精品在线免费观看视频| 国产又粗又猛又爽又黄| 欧美一区二区三区婷婷| 一本一道久久a久久精品| a天堂资源在线观看| 日本中文在线| 国产女人18毛片水真多成人如厕| 国产欧美精品一区二区三区| 99精品在线看| 国内外成人在线| 国产日本欧美一区二区三区| 久久久久久久久久成人| 国产精品一级| 欧美一级大片在线免费观看| 久久久久久久久久久久久久久久久 | 蜜桃av免费在线观看| 免费毛片在线不卡| 亚洲精品一二区| 亚洲国产精品自拍视频| 国产精品极品| 亚洲精品电影网站| 国产精品无码在线| 日韩a级大片| 亚洲国产精品久久91精品| 日韩成人av影院| 中文字幕一区二区三区中文字幕| 日韩一区二区免费电影| 丰满饥渴老女人hd| 凹凸成人在线| 亚洲国产成人一区| 亚洲久久久久久| 亚洲精品3区| 亚洲欧洲高清在线| 美女被到爽高潮视频| 欧美激情在线精品一区二区三区| 亚洲欧美中文日韩在线v日本| 三上悠亚影音先锋| 日韩精品诱惑一区?区三区| 国产亚洲欧洲黄色| а天堂中文在线资源| 亚洲人体av| 欧美激情小视频| 日韩精品乱码久久久久久| 99精品免费| 日本欧美爱爱爱| 国产精品成人久久久| 精品影视av免费| 91蜜桃网站免费观看| 风流老熟女一区二区三区| 99国产精品国产精品毛片| 日本三级中国三级99人妇网站| 91高清在线视频| 亚洲乱码日产精品bd| 成人免费毛片在线观看| 国精产品一区二区三区有限公司 | 欧美大片免费观看在线观看网站推荐 | 国产精品亚洲a| 国产精品久久久久久吹潮| 日韩视频永久免费| 亚洲一区二区乱码| 久久一区二区三区喷水| 久99九色视频在线观看| 69视频免费在线观看| 老鸭窝一区二区久久精品| 高清国语自产拍免费一区二区三区| 四虎精品成人影院观看地址| 成人欧美一区二区三区白人| 国产黄页在线观看| 欧美激情啪啪| 亚洲精品电影网在线观看| 殴美一级黄色片| 国产亚洲永久域名| 91久久久精品| 久草在线免费福利资源| 一区二区三区在线视频免费观看| 妺妺窝人体色www在线小说| 99re8精品视频在线观看| 日韩精品极品在线观看| 污污的视频在线免费观看| 国产精品久久久久久久免费软件| 国产精品一区二区性色av| 色欲av永久无码精品无码蜜桃| 国产精品国产三级国产普通话三级 | 四虎成人av| 国产91精品黑色丝袜高跟鞋| 国产精品特级毛片一区二区三区| 久久久久久一级片| 国产伦精品一区二区三区四区视频_| 久久91超碰青草在哪里看| 日韩电影在线观看中文字幕| 国产探花在线播放| 麻豆成人免费电影| 日本在线高清视频一区| 1024在线看片你懂得| 5月丁香婷婷综合| 五月天综合视频| 午夜综合激情| 国内一区二区在线视频观看| a毛片在线播放| 欧美日韩一区二区三区不卡| 免费黄色在线视频| 99精品国产在热久久婷婷| 国产成人精品日本亚洲11| 欧美尤物美女在线| 日韩欧美一区中文| 久久久无码中文字幕久...| 国模套图日韩精品一区二区| 日韩精品专区在线影院观看| 强制高潮抽搐sm调教高h| 另类av一区二区| 精品国产一区二区三区四区vr| 日本大胆在线观看| 日韩一区二区三区在线视频| 日韩一区二区三区四区视频| 免费高清视频精品| 视频一区二区三区免费观看| 亚洲天堂av影院| 日韩成人在线视频观看| 国产无码精品在线播放| 成人在线视频一区| 国产一级做a爰片久久毛片男| 久久精品一级| 久久99国产精品自在自在app| 99国产精品久久久久99打野战| ㊣最新国产の精品bt伙计久久| 亚洲一级片av| 亚洲中无吗在线| 亚洲综合大片69999| 性欧美videoshd高清| 精品国产a毛片| 久草精品视频在线观看| 97成人超碰视| 国产欧美高清在线| 精品国产91久久久久久浪潮蜜月| 国产精品普通话| 国产盗摄在线观看| 日韩欧美二区三区| 亚洲黄色一区二区| 久久久精品综合| 天天干天天玩天天操| 欧美一区网站| 国产精品一区二区免费| 芒果视频成人app| 最近2019好看的中文字幕免费 | 欧美成人视屏| 日韩欧美综合在线| 国产又大又黄视频| 中文字幕精品一区二区三区精品| 九九九九九伊人| 一区免费在线| 色综合666| 清纯唯美激情亚洲| 欧美亚洲在线视频| 国产欧美久久久久久久久| 亚洲激情视频网站| 中文字幕永久免费视频| 一卡二卡三卡日韩欧美| 亚洲专区区免费| 久久99深爱久久99精品| 欧美一级视频在线播放| 国产免费av一区二区三区| 成人网中文字幕| 性欧美又大又长又硬| 最新亚洲国产精品| 人人妻人人澡人人爽精品日本| 色悠悠久久综合| jizz亚洲少妇| 久久精品男人的天堂| 两性午夜免费视频| 久久综合导航| 99国产精品白浆在线观看免费| 九九热精品视频在线观看| 5566中文字幕一区二区| 韩国三级一区| 久久久久在线观看| 天堂а√在线资源在线| 精品丝袜一区二区三区| www.午夜激情| 欧美午夜寂寞影院| 欧美一级视频免费观看| 亚洲精品视频一区| 天天干天天舔天天操| 99久久精品国产毛片| 中文字幕一区二区三区四| 视频一区视频二区在线观看| 久久久久久久9| 午夜影院欧美| 日本精品免费| 精品国产一区二区三区不卡蜜臂 | 丁香5月婷婷久久| 国产日本欧美一区二区三区在线 | 国产在线观看网站| 日韩激情第一页| 欧美一区二区黄片| 日韩一区二区在线观看| 国产又爽又黄免费软件| 在线观看国产一区二区| 欧美三级韩国三级日本三斤在线观看| 亚洲色图另类专区| 99久久精品久久亚洲精品| 国产欧美一区二区三区沐欲 | 一色屋精品亚洲香蕉网站| xxx在线播放| 95精品视频在线| 国产+高潮+白浆+无码| 国产传媒日韩欧美成人| 日本一本在线视频| 韩国成人福利片在线播放| 国产精品一区二区小说| 免费人成在线不卡| 五月天亚洲视频| 免费在线视频一区| 91日韩视频在线观看| 日韩1区2区3区| 无需播放器的av| 青青草原综合久久大伊人精品优势 | 色嗨嗨av一区二区三区| 日韩精品成人免费观看视频| 狠狠躁18三区二区一区| www亚洲视频| 色哟哟亚洲精品| 青青草视频在线观看免费| 色综合久久99| 老熟妇一区二区三区啪啪| 在线观看免费成人| 国产又粗又黄又爽| 日韩一区二区精品葵司在线| 午夜精品久久久久久久99老熟妇| 日韩丝袜情趣美女图片| 成人高潮片免费视频| 亚洲国产成人精品电影| 欧美扣逼视频| 中文字幕亚洲欧美在线| 黄网站免费在线播放| 欧美精品在线观看| 2019中文字幕在线电影免费| 欧美一级视频一区二区| 欧美三级网址| 国产三级精品网站| 亚洲国产视频二区| 久久99精品久久久久子伦 | 免费观看国产视频在线| 国内久久精品| 男人天堂1024| 久久精品国产99久久6| 日韩欧美中文视频| 成人av网站在线观看| 国产特黄级aaaaa片免| 国产精品国产自产拍高清av王其 | 丰满饥渴老女人hd| 91美女片黄在线观看| avhd101老司机| 亚洲精品视频在线观看免费 | 欧美精品久久久久久久久老牛影院| 国产免费不卡av| 亚洲精品国产精品国自产观看浪潮| 裸体xxxx视频在线| 久久这里只有精品99| 国产直播在线| 国产日韩在线播放| 奇米777国产一区国产二区| 亚洲欧洲在线一区| 午夜精品网站| 日本va中文字幕| 国产精品自拍网站| 亚洲第一成人网站| 亚洲理论在线观看| 国产字幕在线观看| 欧美变态tickle挠乳网站| 国产一二三区在线视频| 欧美疯狂xxxx大交乱88av| 欧美精品日日操| 国产精品久久久对白| 日本大胆欧美| 玩弄中年熟妇正在播放| 国内精品写真在线观看| 白白色免费视频| 亚洲丶国产丶欧美一区二区三区| 中文字幕在线观看国产| 亚洲国产女人aaa毛片在线| 国产在线观看91| 国产精彩精品视频| 欧美自拍视频| 欧美中文字幕在线观看视频| 美女国产一区二区| 国产免费一区二区三区网站免费| 亚洲综合在线第一页| 国产一区二区小视频| 尤物yw午夜国产精品视频| 精精国产xxxx视频在线播放| 91亚洲精品丁香在线观看| 日产午夜精品一线二线三线| 亚洲午夜无码av毛片久久| 丰满少妇久久久久久久| www.xxxx日本| 欧美日韩三级一区二区| 国产一级在线观看| 欧美一级高清免费| 日韩欧美影院| 欧美爱爱视频免费看| 粉嫩蜜臀av国产精品网站| 欧美成人一区二区三区高清| 777a∨成人精品桃花网| 国产小视频免费在线网址| 97激碰免费视频| 国产精品qvod| 草草视频在线免费观看| 国产成人av电影| 欧美日韩一级在线观看| 欧美成人伊人久久综合网| 成人福利片网站| 91最新在线免费观看| 91精品久久久久久久久久不卡| 日本不卡一区在线| 国产精品国产a| 国产精品呻吟久久| 乱亲女秽乱长久久久| 久久国产精品免费一区二区三区| 干日本少妇视频| 国产精品中文有码| 久草视频免费在线| 欧美精品一区二区三区蜜臀| 麻豆av在线免费观看| 国产精品三区www17con| 亚洲欧洲一级| 极品粉嫩小仙女高潮喷水久久| 天天色综合成人网| 日本午夜在线| 国产精品一区二区久久国产| 国产精品不卡| 中文字幕人妻熟女人妻a片| 亚洲综合一区二区精品导航| 高清毛片aaaaaaaaa片| 国产91精品久久久久| 成人精品影院| 国产女同无遮挡互慰高潮91| 亚洲伊人伊色伊影伊综合网| 亚洲欧美色视频| 国产精品日韩精品| 永久91嫩草亚洲精品人人| 深田咏美中文字幕| 色香蕉成人二区免费| 免费网站看v片在线a| av资源站久久亚洲| 免费欧美在线| 糖心vlog免费在线观看| 精品国产乱码久久久久久1区2区| 毛片免费看不卡网站| 亚洲色图自拍| 豆国产96在线|亚洲| 7799精品视频天天看| 久久精品国产成人精品| 豆花视频一区二区| 日本熟妇人妻中出| 亚洲精选在线视频| 欧美日本网站| 5g影院天天爽成人免费下载| 亚洲尤物精选| 九九热最新地址| 日韩国产中文字幕| 国产一区二区三区免费观看在线 | 国产黄a三级三级| 精品国产乱码久久久久久老虎| 欧美不卡高清一区二区三区| 蜜桃视频成人在线观看| 久久在线免费观看| 不卡视频在线播放| 国产精品亚洲片夜色在线| 黄色av一区| 国产精品久久久免费看|