Redis分散式鎖進化史
近兩年來微服務變得越來越熱門,越來越多的應用部署在分散式環境中,在分散式環境中,數據一致性是一直以來需要關注並且去解決的問題,分散式鎖也就成為了一種廣泛使用的技術,常用的分散式實現方式為Redis,Zookeeper,其中基於Redis的分散式鎖的使用更加廣泛。
但是在工作和網路上看到過各個版本的Redis分散式鎖實現,每種實現都有一些不嚴謹的地方,甚至有可能是錯誤的實現,包括在代碼中,如果不能正確的使用分散式鎖,可能造成嚴重的生產環境故障,本文主要對目前遇到的各種分散式鎖以及其缺陷做了一個整理,並對如何選擇合適的Redis分散式鎖給出建議。
各個版本的Redis分散式鎖
V1.0
tryLock(){
SETNX Key 1
EXPIRE Key Seconds
}
release(){
DELETE Key
}
這個版本應該是最簡單的版本,也是出現頻率很高的一個版本,首先給鎖加一個過期時間操作是為了避免應用在服務重啟或者異常導致鎖無法釋放後,不會出現鎖一直無法被釋放的情況。
這個方案的一個問題在於每次提交一個Redis請求,如果執行完第一條命令後應用異常或者重啟,鎖將無法過期,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條命令),但是如果Redis僅執行了一條命令後crash或者發生主從切換,依然會出現鎖沒有過期時間,最終導致無法釋放。
另外一個問題在於,很多同學在釋放分散式鎖的過程中,無論鎖是否獲取成功,都在finally中釋放鎖,這樣是一個鎖的錯誤使用,這個問題將在後續的V3.0版本中解決。
針對鎖無法釋放問題的一個解決方案基於GETSET命令來實現
V1.1 基於GETSET
tryLock(){
NewExpireTime=CurrentTimestamp+ExpireSeconds
if(SETNX Key NewExpireTime Seconds){
oldExpireTime = GET(Key)
if( oldExpireTime < CurrentTimestamp){
NewExpireTime=CurrentTimestamp+ExpireSeconds
CurrentExpireTime=GETSET(Key,NewExpireTime)
if(CurrentExpireTime == oldExpireTime){
return 1;
}else{
return 0;
}
}
}
}
release(){
DELETE key
}
思路:
1、SETNX(Key,ExpireTime)獲取鎖
2、如果獲取鎖失敗,通過GET(Key)返回的時間戳檢查鎖是否已經過期
3、GETSET(Key,ExpireTime)修改Value為NewExpireTime
4、檢查GETSET返回的舊值,如果等於GET返回的值,則認為獲取鎖成功
注意:這個版本去掉了EXPIRE命令,改為通過Value時間戳值來判斷過期
問題:
1、在鎖競爭較高的情況下,會出現Value不斷被覆蓋,但是沒有一個Client獲取到鎖
2、在獲取鎖的過程中不斷的修改原有鎖的數據,設想一種場景C1,C2競爭鎖,C1獲取到了鎖,C2鎖執行了GETSET操作修改了C1鎖的過期時間,如果C1沒有正確釋放鎖,鎖的過期時間被延長,其它Client需要等待更久的時間
V2.0 基於SETNX
tryLock(){
SETNX Key 1 Seconds
}
release(){
DELETE Key
}
Redis 2.6.12版本後SETNX增加過期時間參數,這樣就解決了兩條命令無法保證原子性的問題。但是設想下面一個場景:
1、C1成功獲取到了鎖,之後C1因為GC進入等待或者未知原因導致任務執行過長,最後在鎖失效前C1沒有主動釋放鎖
2、C2在C1的鎖超時後獲取到鎖,並且開始執行,這個時候C1和C2都同時在執行,會因重複執行造成數據不一致等未知情況
3、C1如果先執行完畢,則會釋放C2的鎖,此時可能導致另外一個C3進程獲取到了鎖
大致的流程圖