前言
默認的加鎖邏輯是非公平的。
在加鎖失敗時,線程會進入 while 循環,一直嘗試獲得鎖,這時候是多線程進行競爭。就是說誰搶到就是誰的。
Redisson 提供了 公平鎖 機制,使用方式如下:
RLock fairLock = redisson.getFairLock("anyLock");
// 最常見的使用方法
fairLock.lock();
下面一起看下公平鎖是如何實現的?
公平鎖
相信小伙伴們看過前面的文章,已經輕車熟路了,直接定位到源碼方法:RedissonFairLock#tryLockInnerAsync
。
好家伙,這一大塊代碼,我截圖也截不完,咱們直接分析 lua 腳本。
PS:雖然咱不懂 lua,但是這一堆堆的 if else 咱們大概還是能看懂的。
因為 debug 發現 command == RedisCommands.EVAL_LONG
,所以直接看下面一部分。
這么長,連呼好幾聲好家伙!
先來看看參數都有啥?
- KEYS[1]:加鎖的名字,
anyLock
; - KEYS[2]:加鎖等待隊列,
redisson_lock_queue:{anyLock}
; - KEYS[3]:等待隊列中線程鎖時間的 set 集合,
redisson_lock_timeout:{anyLock}
,是按照鎖的時間戳存放到集合中的; - ARGV[1]:鎖超時時間 30000;
- ARGV[2]:UUID:ThreadId 組合
a3da2c83-b084-425c-a70f-5d9a08b37f31:1
; - ARGV[3]:threadWaitTime 默認 300000;
- ARGV[4]:currentTime 當前時間戳。
加鎖隊列和集合是含有大括號的字符串。{XXXX} 是指這個 key 僅使用 XXXX 用來計算 slot 的位置。
Lua 腳本分析
上面的 lua 腳本是分為幾塊的,咱們分別從不同的角度看下上面代碼的執行。
首次加鎖(Thread1)
第一部分,因為是首次加鎖,所以等待隊列為空,直接 跳出循環。這一部分執行結束。
第二部分:
- 當鎖不存在,等待隊列為空或隊首是當前線程,兩個條件都滿足時,進入內部邏輯;
- 從等待隊列和超時集合中刪除當前線程,這時候等待隊列和超時集合都是空的,不需要任何操作;
- 減少隊列中所有等待線程的超時時間,也不需要任何操作;
- 加鎖並設置超時時間。
執行完這里就 return
了。所以后面幾部分就暫時不看了。
相當於下面兩個命令(整個 lua 腳本都是原子的!):
> hset anyLock a3da2c83-b084-425c-a70f-5d9a08b37f31:1 1
> pexpire anyLock 30000
Thread2 加鎖
當 Thread1 加鎖完成之后,此時 Thread2 來加鎖。
Thread2 可以是本實例其他線程,也可以是其他實例的線程。
第一部分,雖然鎖被 Thread1 占用了,但是等待隊列是空的,直接跳出循環。
第二部分,鎖存在,直接跳過。
第三部分,線程是否持鎖,沒有持鎖,直接跳過。
第四部分,線程是否在等待隊列中,Thread2 才來加鎖,不在里面,直接跳過。
Thread2 最后會來到這里:
- 從線程等待隊列
redisson_lock_queue:{anyLock}
中獲取最后一個線程; - 因為等待隊列是空的,所以直接獲取當前鎖的剩余時間
ttl anyLock
; - 組裝超時時間 timeout = ttl + 300000 + 當前時間戳,這個 300000 是默認
60000*5
; - 使用 zadd 將 Thread2 放到等待線程有序集合,然后使用 rpush 將 Thread2 再放到等待隊列中。
zadd KEYS[3] timeout ARGV[2]
這里使用 zadd 命令分別放置的是,redisson_lock_timeout:{anyLock}
,超時時間戳(1624612689520),線程(UUID2:Thread2)。
其中超時時間戳當分數,用來在有序集合中排序,表示加鎖的順序。
Thread3 加鎖
Thread1 占有了鎖,Thread2 在等待,此時線程 3 來了。
獲取 firstThreadId2 此時隊列是有線程的是 UUID2:Thread2。
判斷 firstThreadId2 的分數(超時時間戳)是不是小於當前時間戳:
- 小於等於則說明超時了,移除 firstThreadId2;
- 大於,則會進入后續判斷。
第二、三、四部分都不滿足條件。
Thread3 最后也會來到這里:
- 從線程等待隊列
redisson_lock_queue:{anyLock}
中獲取最后一個線程; - 最后一個線程存在,且不是自己,則 ttl = lastThreadId 超時時間戳 - 當前時間戳,就是看最后一個線程還有多久超時;
- 組裝超時時間 timeout = ttl + 300000 + 當前時間戳,這個 300000 是默認
60000*5
,在最后一個線程的超時時間上加上 300000 以及當前時間戳,就是 Thread3 的超時時間戳。 - 使用 zadd 將 Thread3 放到等待線程有序集合,然后使用 rpush 將 Thread3 再放到等待隊列中。
總結
本文主要總結了公平鎖的加鎖邏輯,這涉及到比較多的 Redis 操作,做一下簡要總結:
- Redis Hash 數據結構:存放當前鎖,Redis Key 就是鎖,Hash 的 field 是加鎖線程,Hash 的 value 是 重入次數;
- Redis List 數據結構:充當線程等待隊列,新的等待線程會使用 rpush 命令放在隊列右邊;
- Redis sorted set 有序集合數據結構:存放等待線程的順序,分數 score 用來是等待線程的超時時間戳。
需要理解的就是這里會額外添加一個等待隊列,以及有序集合。
對照着 Java 公平鎖源碼閱讀,理解起來效果更好。