Redisson 分佈式鎖源碼——公平鎖加鎖

 前言

默認的加鎖邏輯是非公平的。程序員

在加鎖失敗時,線程會進入 while 循環,一直嘗試得到鎖,這時候是多線程進行競爭。就是說誰搶到就是誰的。面試

Redisson 提供了 公平鎖 機制,使用方式以下:redis

RLock fairLock = redisson.getFairLock("anyLock");
// 最多見的使用方法
fairLock.lock();

下面一塊兒看下公平鎖是如何實現的?數據結構

公平鎖

直接定位到源碼方法:RedissonFairLock#tryLockInnerAsync多線程

好傢伙,這一大塊代碼,我截圖也截不完,我們直接分析 lua 腳本。ide

PS:雖然咱不懂 lua,可是這一堆堆的 if else 我們大概仍是能看懂的。lua

由於 debug 發現 command == RedisCommands.EVAL_LONG,因此直接看下面一部分。spa

6ba8af6524a793111523c08d34b4c8c6.jpeg

這麼長,連呼好幾聲好傢伙!線程

先來看看參數都有啥?debug

  1. KEYS[1]:加鎖的名字,anyLock
  2. KEYS[2]:加鎖等待隊列,redisson_lock_queue:{anyLock}
  3. KEYS[3]:等待隊列中線程鎖時間的 set 集合,redisson_lock_timeout:{anyLock},是按照鎖的時間戳存放到集合中的;
  4. ARGV[1]:鎖超時時間 30000;
  5. ARGV[2]:UUID:ThreadId 組合 a3da2c83-b084-425c-a70f-5d9a08b37f31:1
  6. ARGV[3]:threadWaitTime 默認 300000;
  7. ARGV[4]:currentTime 當前時間戳。

3a4066fbc137cfc804d17359b2476a95.png

加鎖隊列和集合是含有大括號的字符串。{XXXX} 是指這個 key 僅使用 XXXX 用來計算 slot 的位置。

Lua 腳本分析

上面的 lua 腳本是分爲幾塊的,我們分別從不一樣的角度看下上面代碼的執行。

首次加鎖(Thread1)

fa2475fde103aca06b77168347b5e9fb.jpeg

第一部分,由於是首次加鎖,因此等待隊列爲空,直接 跳出循環。這一部分執行結束。

af7667e701ab29c04ef1da6f55c086ab.jpeg

第二部分:

  1. 當鎖不存在,等待隊列爲空或隊首是當前線程,兩個條件都知足時,進入內部邏輯;
  2. 從等待隊列和超時集合中刪除當前線程,這時候等待隊列和超時集合都是空的,不須要任何操做;
  3. 減小隊列中全部等待線程的超時時間,也不須要任何操做;
  4. 加鎖並設置超時時間。

執行完這裏就 return 了。因此後面幾部分就暫時不看了。

至關於下面兩個命令(整個 lua 腳本都是原子的!):

> hset anyLock a3da2c83-b084-425c-a70f-5d9a08b37f31:1 1
> pexpire anyLock 30000

021bc4869ca202129ca2e5edce6469f2.png

Thread2 加鎖

當 Thread1 加鎖完成以後,此時 Thread2 來加鎖。

Thread2 能夠是本實例其餘線程,也能夠是其餘實例的線程。

7c16ee6f99723db2edc20615fdd29cae.jpeg

第一部分,雖然鎖被 Thread1 佔用了,可是等待隊列是空的,直接跳出循環。

739607cca316e7ab557f726f0e0b2b57.jpeg

第二部分,鎖存在,直接跳過。

cadb63572cf819a024879cea4938a649.jpeg

第三部分,線程是否持鎖,沒有持鎖,直接跳過。

第四部分,線程是否在等待隊列中,Thread2 纔來加鎖,不在裏面,直接跳過。

f872a97ff6da1ad242b8c60f2a3bacb8.jpeg

Thread2 最後會來到這裏:

  1. 從線程等待隊列 redisson_lock_queue:{anyLock} 中獲取最後一個線程;
  2. 由於等待隊列是空的,因此直接獲取當前鎖的剩餘時間 ttl anyLock
  3. 組裝超時時間 timeout = ttl + 300000 + 當前時間戳,這個 300000 是默認 60000*5
  4. 使用 zadd 將 Thread2 放到等待線程有序集合,而後使用 rpush 將 Thread2 再放到等待隊列中。

zadd KEYS[3] timeout ARGV[2]

這裏使用 zadd 命令分別放置的是,redisson_lock_timeout:{anyLock},超時時間戳(1624612689520),線程(UUID2:Thread2)。

其中超時時間戳當分數,用來在有序集合中排序,表示加鎖的順序。

dca68c31dae1ff3f76d69495abcc2d1a.png

Thread3 加鎖

a5237a14fd30907924a9e457b8b4b40f.png

Thread1 佔有了鎖,Thread2 在等待,此時線程 3 來了。

c78d07e84051502061ce97bc41f85a05.jpeg

獲取 firstThreadId2 此時隊列是有線程的是 UUID2:Thread2。

判斷 firstThreadId2 的分數(超時時間戳)是否是小於當前時間戳:

  1. 小於等於則說明超時了,移除 firstThreadId2;
  2. 大於,則會進入後續判斷。

第2、3、四部分都不知足條件。

0b114442b97f7a23be4f285363081e0a.jpeg

Thread3 最後也會來到這裏:

  1. 從線程等待隊列 redisson_lock_queue:{anyLock} 中獲取最後一個線程;
  2. 最後一個線程存在,且不是本身,則 ttl = lastThreadId 超時時間戳 - 當前時間戳,就是看最後一個線程還有多久超時;
  3. 組裝超時時間 timeout = ttl + 300000 + 當前時間戳,這個 300000 是默認 60000*5,在最後一個線程的超時時間上加上 300000 以及當前時間戳,就是 Thread3 的超時時間戳。
  4. 使用 zadd 將 Thread3 放到等待線程有序集合,而後使用 rpush 將 Thread3 再放到等待隊列中。

928ae2637bcfa1f38b553ffa6b1fed97.png

總結

本文主要總結了公平鎖的加鎖邏輯,這涉及到比較多的 Redis 操做,作一下簡要總結:

  1. Redis Hash 數據結構:存放當前鎖,Redis Key 就是鎖,Hash 的 field 是加鎖線程,Hash 的 value 是 重入次數;
  2. Redis List 數據結構:充當線程等待隊列,新的等待線程會使用 rpush 命令放在隊列右邊;
  3. Redis sorted set 有序集合數據結構:存放等待線程的順序,分數 score 用來是等待線程的超時時間戳。

deae57129c833505afebc73f598e3fde.png

須要理解的就是這裏會額外添加一個等待隊列,以及有序集合。

對照着 Java 公平鎖源碼閱讀,理解起來效果更好。

最後

最近我整理了整套《JAVA核心知識點總結》,說實話 ,做爲一名Java程序員,不論你需不須要面試都應該好好看下這份資料。拿到手老是不虧的~個人很多粉絲也所以拿到騰訊字節快手等公司的Offer

Java進階之路羣,找管理員獲取哦-!

99b8a0b9c6f3f0b4312c6b27e245f3b0.png

相關文章
相關標籤/搜索