大家好,我是此林。
今天來分享Redisson分布式鎖源碼。還是一樣,我們用?問題驅動?的方式展開講述。
1. redis 中如何使用 lua 腳本?
Redis內置了lua解釋器,lua腳本有兩個好處:
1. 減少多次Redis命令的網絡傳輸開銷。(當然也可以使用pipline命令)
2. lua腳本所有命令能保證原子性,隔離性(Redis單線程),失敗回滾
綜上所述,Redis中如果想要實現事務操作,可以使用lua腳本。
Redis 本身也可以使用 MULTI + WATCH樂觀鎖?來實現,但是它只能保證命令執行的順序性,無法保證失敗回滾,無法保證原子性。
所以,一般推薦使用 lua 腳本。
使用案例:
現在我們要去執行redis命令:
HSET info name john
1. lua腳本
local hash_key = KEYS[1] -- 哈希結構的鍵名(外部傳入)
local key = ARGV[1] -- 哈希字段(外部傳入)
local value = ARGV[2] -- 哈希字段值(外部傳入)return redis.call('HSET', hash_key, key, value)
因為KEYS[1]、ARGV[1]等都是外部傳入,所以可以簡化。
return redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
redis.call()就是執行redis命令。
2. redis 命令
EVAL "return redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])" 1 info name john
這里的1代表傳入一個key。
2. 如何使用Redisson?
這里的?
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
是獲取鎖,第一個1表示:鎖超時等待時間,在1秒內會不斷重試獲取鎖,直到獲取到。
第二個10表示:鎖釋放時間,為10秒(防止java服務獲取到鎖后,突然宕機,導致redis鎖永遠不會被釋放,避免造成死鎖問題。)
3. Redisson源碼?
3.1. Redisson如何實現鎖重入?
其實歸根結底,就是這段代碼。Redisson本質上是使用hash結構來標志鎖的,可能我們經常聽到說用setnx命令來實現分布式鎖,但是setnx無法實現鎖的重入。
所以Redisson用 hash(計數) + lua腳本(原子性)實現可重入分布式鎖。
先說明下參數:
KEYS[1]:hash結構的鍵名,也就是我們之前手動指定的 anylock
ARGV[1]:鎖的釋放時間
ARGV[2]:hash結構的字段的鍵名,UUID:線程id
我們直接去看redis:
anyLock這個hash結構里,
有字段的key為c3341b71-6edd-4db8-b626-9135cf727fd4:1,value為1
了解了鎖的結構后,我們再來看lua腳本。
一圖勝千言,總的來說,
第一個if:處理線程第一次獲取鎖
第二個if:處理線程重入獲取鎖
最后:發現鎖已經被占有,返回剩余ttl(過期時間)。
3.2. 如果搶鎖失敗呢?
之前說的鎖的設置,其實就是圖中框起來的方法里的實現。
接下來,如果ttl為null,搶鎖成功了,直接返回true。
如果ttl不為null,說明搶鎖失敗了,會去計算等待時間是否充足。
這里的等待時間就是我們之前手動設置的1秒鐘。
如果鎖等待時間還充足,那么執行它會去用pub-sub機制去?訂閱鎖釋放事件。(避免輪詢 Redis 造成的性能損耗。)
如果訂閱超時,觸發失敗回調,返回false。?
那么訂閱成功了之后呢?會再次嘗試搶鎖。
還不行,那只能信號量掛起,具體通過?Semaphore
(getLatch()
)掛起當前線程,等待鎖釋放的 Pub/Sub 通知。?
總結一下搶鎖流程:
搶鎖成功,直接返回;搶鎖失敗,pub/sub機制訂閱鎖釋放事件,通過信號量掛起線程,直到收到鎖釋放的消息才被喚醒。
3.3. 解鎖流程是怎么樣的?
看下圖。本質還是那一段lua腳本。
先看下鎖是不是線程自己的,不是的話直接返回null。
如果鎖是自己的,計數器減1。
如果減1操作后還大于0,說明重入的還沒完,刷新鎖的超時釋放時間。
如果減1后小于等于0,直接刪除,發布鎖刪除事件。
- 返回nil表示鎖不屬于當前線程,應拋出異常。
- 返回0表示鎖未完全釋放,僅更新了過期時間。
- 返回1表示鎖已釋放,并發布事件。
3.4. Redisson的看門狗機制?
看門狗主要用于 ?解決鎖的自動續期問題,避免因業務執行時間過長導致鎖超時自動釋放。
注意一點:如果我們顯式指定了leaseTime參數,看門狗機制就不會生效。這時候鎖的過期時間由用戶控制。
像我們之前手動指定了鎖釋放時間10秒,它就不會走看門狗機制,Redisson默認鎖釋放時間是30秒。(見下圖,單位毫秒)
在scheduledExpirationRenewal方法里,其實就是用了netty的時間輪進行定時任務調度,每隔10秒重置鎖時間為30秒,直到業務執行結束。
每次續期成功后,會遞歸調用?renewExpiration()
,形成 ?無限續期鏈,直到鎖被釋放(主動釋放或者客戶端宕機)。
關于??時間輪,這是一種高效的定時任務調度設計。
感興趣的朋友可以去看下之前寫的文章:
時間輪:XXL-JOB 高效、精準定時任務調度實現思路分析_xxljob fasttriggerpool slowtrigger-CSDN博客
至于為什么不使用ScheduledThreadPoolExecutor?
是因為ScheduledThreadPoolExecutor的底層結構:基于優先級隊列(堆實現)。插入任務的時間復雜度?O(log n)
,每次插入需調整堆結構。
使用時間輪插入任務的時間復雜度為?O(1)
,直接哈希到時間槽(Bucket),并且支持同一槽內任務批量觸發。
3.5. Redisson怎么解決死鎖的?
主要就是設置了鎖的超時釋放時間,客戶端宕機了會自動超時釋放。
然后還有一點支持重入,如果同一個線程兩次去獲取鎖,因為支持重入,第二次就不會阻塞等待自己釋放鎖了。
至于說看門狗機制會無限續期,客戶端宕機了就續期不了,不會導致死鎖。
那你說:業務要是無限阻塞,永遠執行不完呢?
這個也不大可能,為什么業務會無限阻塞?這個時候肯定需要人工介入去排查問題了。
今天的分享就到這里了,我是此林。
關注我吧,帶你看不一樣的世界!