Redission
1、原理
2、源碼中加鎖lua代碼
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);
分析:
1、為什么要使用lua語言
因為一大堆復雜的業務邏輯,可以通過封裝在lua腳本中發送給redis,保證這段復雜業務邏輯執行的原子性
2、lua字段解釋
KEYS[1]:表示你加鎖的那個key,比如說
RLock lock = redisson.getLock(“myLock”);
這里你自己設置了加鎖的那個鎖key就是“myLock”。
ARGV[1]:表示鎖的有效期,默認30s
ARGV[2]:表示表示加鎖的客戶端ID,類似於下面這樣:
8743c9c0-0795-4907-87fd-6c719a6b4586:1
3、加鎖機制
分析:
lua中第一個if判斷語句,就是用“exists myLock”命令判斷一下,如果你要加鎖的那個鎖key不存在的話,你就進行加鎖。
如何加鎖呢?很簡單,用下面的hset命令:
hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
此時的myLock鎖key的數據結構是:
myLock: { 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1 }
接着會執行“pexpire myLock 30000”命令,設置myLock這個鎖key的生存時間是30秒(默認)
4、鎖互斥機制
如果在這個時候,另一個客戶端(客戶端2)來嘗試加鎖,執行了同樣的一段lua腳本,會怎樣呢?
第一個if判斷會執行“exists myLock”,發現myLock這個鎖key已經存在了。
接着第二個if判斷會執行“hexists mylock 客戶端id”,來判斷myLock鎖key的hash數據結構中,是否包含客戶端2的ID,但是明顯不是的,因為那里包含的是客戶端1的ID。
所以,客戶端2會獲取到pttl myLock返回的一個數字,這個數字代表了myLock這個鎖key的剩余生存時間。
比如還剩15000毫秒的生存時間。此時客戶端2會進入一個while循環,不停的嘗試加鎖。
5、可重入加鎖機制
如果客戶端1已經持有這把鎖,可重入的加鎖會怎么樣呢
#重入加鎖 RLock lock = redisson.getLock("myLock") lock.lock(); //業務代碼 lock.lock(); //業務代碼 lock.unlock(); lock.unlock();
分析上面lua代碼
第一個if判斷不成立,“exists myLock” 會顯示鎖key已經存在了
第二個if會成立,因為myLock的hash數據結構中包含的客戶端1的ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
此時就會執行可重入加鎖的邏輯,用incrby這個命令,對客戶端1的加鎖次數,累加1:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
此時myLock數據結構變為下面這樣:
myLock: { 8743c9c0-0795-4907-87fd-6c719a6b4586:1 2 }
6、釋放鎖機制
Redisson釋放鎖的lua代碼
if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end; if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
執行lock.unlock(),就可以釋放分布式鎖,此時的業務邏輯也是非常簡單的。
就是每次都對myLock數據結構中的那個加鎖次數減1。如果發現加鎖次數是0了,說明這個客戶端已經不再持有鎖了,此時就會用:“del myLock”命令,從redis里刪除這個key。
然后另外的客戶端2就可以嘗試完成加鎖了。
這就是所謂的分布式鎖的開源Redisson框架的實現機制。
一般我們在生產系統中,可以用Redisson框架提供的這個類庫來基於redis進行分布式鎖的加鎖與釋放鎖。
7、watch dog自動延期機制
1、客戶端1加鎖的鎖key默認生存時間才30秒,如果超過了30秒,客戶端1還想一直持有這把鎖,怎么辦呢?
Redisson中客戶端1一旦加鎖成功,就會啟動一個watch dog看門狗,他是一個后台線程,會每隔10秒檢查一下,如果客戶端1還持有鎖key,那么就會不斷的延長鎖key的生存時間。
2、如果負責存儲這個分布式鎖的Redission節點宕機后,而且這個鎖正好處於鎖住的狀態時,這個鎖會出現鎖死的狀態,為了避免這種情況的發生,Redisson提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期。默認情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。
另外Redisson 還提供了可以指定leaseTime參數的加鎖方法來指定加鎖的時間。超過這個時間后鎖便自動解開了。不會延長鎖的有效期!!!
來看源碼:
可以看到,這個加的分布式鎖的超時時間默認是30秒.但是還有一個問題,那就是這個看門狗,多久來延長一次有效期呢?我們往下看
從圖中可以看出,獲取鎖成功就會開啟一個定時任務,也就是watchdog,定時任務會定期檢查去續期
(這里定時用的是netty-common包中的HashedWheelTimer)
從圖中我們明白,該定時調度每次調用的時間差是internalLockLeaseTime / 3。也就10秒.
通過源碼分析我們知道,默認情況下,加鎖的時間是30秒.如果加鎖的業務沒有執行完,那么有效期到 30-10 = 20秒的時候,就會進行一次續期,把鎖重置成30秒.那這個時候可能又有同學問了,那業務的機器萬一宕機了呢?宕機了定時任務跑不了,就續不了期,那自然30秒之后鎖就解開了唄.
缺點
最大的問題,就是如果你對某個redis master實例,寫入了myLock這種鎖key的value,此時會異步復制給對應的master slave實例。但是這個過程中一旦發生redis master宕機,主備切換,redis slave變為了redis master。接着就會導致,客戶端2來嘗試加鎖的時候,在新的redis master上完成了加鎖,而客戶端1也以為自己成功加了鎖。此時就會導致多個客戶端對一個分布式鎖完成了加鎖。這時系統在業務上一定會出現問題,導致臟數據的產生。所以這個就是redis cluster,或者是redis master-slave架構的主從異步復制導致的redis分布式鎖的最大缺陷:在redis master實例宕機的時候,可能導致多個客戶端同時完成加鎖。
參考地址
https://www.cnblogs.com/yuxiang1/archive/2019/03/13/10527028.html
https://blog.csdn.net/lzhcoder/article/details/88387751
https://blog.csdn.net/lzhcoder/article/details/88387751