Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式
追求效能極致:Redis6.0的多執行緒模型
追求效能極致:使用者端快取帶來的革命
Redis系列8:Bitmap實現億萬級資料計算
Redis系列9:Geo 型別賦能億級地圖位置計算
Redis系列10:HyperLogLog實現海量資料基數統計
Redis系列11:記憶體淘汰策略
Redis系列12:Redis 的事務機制
分散式鎖,即分散式系統中的鎖,我們通過鎖解決 控制共用資源存取 的問題,來保證只有一個執行緒可以存取被保護的資源。
等等,本篇基於Redis角度進行討論
SETNX 是 set if not exists 的縮寫,當且僅當 key 不存在時,則設定 value 給這個key。若給定的 key 已經存在,則 SETNX 不做任何動作。
命令的返回值說明:
舉例說明:setnx lock.key lock.value
> SETNX lock.user_063105015 1
(integer) 1 # 獲取編號為 063105015 使用者成功
如果已經被獲取過了,則獲取失敗
> SETNX lock.user_063105015 1
(integer) 0 # 獲取編號為 063105015 使用者失敗
獲取key的值,如果存在,則返回;如果不存在,則返回nil
# 獲取成功
> GET lock.user_063105015
"1"
# 獲取失敗
> GET lock.user_123456789
(nil)
原子的設定值的辦法,對key設定newValue這個值,並且返回key原來的舊值。
# 重置使用者資訊
> getset lock.user_063105015 0
"1" # 原值為1
# 再次重置
> getset lock.user_063105015 1
"0" # 原值為0
> DEL lock.user_063105015
(integer) 1
具體執行流程如下:
可能會因為一些場景,造成鎖無法釋放,如下:
超時釋放其實就是重置,目的是避免因為各種原因導致的鎖長時間無法釋放。
做法就是我們給鎖加個過期時間(EXPIRE Time):
# 給使用者 063105015 加鎖
> SETNX lock.user_063105015 1
(integer) 1
# 設定過期時間,到時間沒刪除則自動釋放
> EXPIRE lock.user_063105015 120 # 120秒之後自動釋放
(integer) 1
為了保證執行時的原子性,Redis 官方擴充套件了 SET 命令,既能滿足獲取物件,又能保證設定超時的時間語意。
避免出現了獲取鎖完成之後,執行超時設定失敗微軟無法釋放鎖的情況。保證要麼都成功,要麼都不執行。
# 範例如下:
SET lock.user_063105015 1 NX PX 60000
經常會出現一種情況,就是你獲取到鎖之後,因為各種原因(比如你的服務執行緒故障、網路抖動 等等),沒有執行完成,或者沒有釋放鎖,
這時候鎖也過了 EXPIRE TIME,就自動釋放了。當另外一個執行緒開鎖成功,你的執行緒響應過來了,把人家的鎖給釋放了,這樣就有問題了。
為了避免這種操作,我們要對同一個的鎖做唯一識別碼,在釋放鎖之前,先判斷下是不是自己設定的那個鎖,如下:
# 設定10086專用值
> SET lock.user_063105015 10086 NX PX 60000
OK
# 設定成功,獲取檢查確實是10086
> get lock.user_063105015
"10086"
# 虛擬碼:刪除前進項確認是不是自己加的那個鎖
if ( redis.get("lock.user_063105015").equals("10086")) {
redis.del("lock.user_063105015"); // 只有對比成功才進行刪除,釋放鎖
}
可重入鎖可以理解為重新進入,由多於一個任務並行使用,而不必擔心資料錯誤。
這邊說說可重入鎖,比如你執行執行緒的方案a獲取鎖之後,你的a方法後,執行緒繼續執行b方法也需要獲取鎖,如果這時候不可重入,
執行緒就需要等待鎖的釋放,進入爭搶。
這邊的解法就是對執行緒加鎖的鎖值進行增減,同一個執行緒的方法遇到加鎖則鎖值+1,遇到退鎖則鎖值-1,當前僅當鎖值=0的時候,說明這個鎖真正的被釋放了。
Java中的Redisson 類庫就是通過 Redis Hash 來實現可重入鎖。
加鎖的邏輯
我們可以使用 Redis hash 結構實現,key 表示被鎖的共用資源, hash 結構的 fieldKey 的 value 則儲存加鎖的次數。
實現如下( KEYS1 = "lock.user_063105015", ARGV [10000,uuid):
KEYS[1] = key的值
ARGV[1]) = 持有鎖的時間
ARGV[2] = getLockName(threadId) 下面id就算系統在啟動的時候會全域性生成的uuid 來作為當前程序的id,加上執行緒id就是getLockName(threadId)了,可以理解為:程序ID+系統ID = ARGV[2]
# 1 為 true
# 0 為 false
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', 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]);
引數說明
程式說明