redis分布式鎖,Lua,Lua腳本,lua redis,redis lua 分布式鎖,redis setnx ,redis分布式鎖, Lua腳本在redis分布式鎖場景的運用。
鎖和分布式鎖
鎖是什么?
鎖是一種可以封鎖資源的東西。這種資源通常是共享的,通常會發生使用競爭的。
為什么需要鎖?
需要保護共享資源正常使用,不出亂子。
比方說,公司只有一間廁所,這是個共享資源,大家需要共同使用這個廁所,所以避免不了有時候會發生競爭。如果一個人正在使用,另外一個人進去了,咋辦呢?如果兩個人同時鑽進了一個廁所,那該怎么辦?結果如何?誰先用,還是一起使用?特別的,假如是一男一女同時鑽進了廁所,事情會怎樣呢?反正我是不懂……
如果這個時候廁所門前有個鎖,每個人都沒法隨便進入,而是需要先得到鎖,才能進去。而得到這個鎖,就需要里邊的人先出來。這樣就可以保證同一時刻,只有一個人在使用廁所,這個人在上廁所的期間不會有不安全的事情發生,不會中途被人闖進來了。
Java中的鎖
在 java 編碼的時候,為了保護共享資源,使得多線程環境下,不會出現“不好的結果”。我們可以使用鎖來進行線程同步。於是我們可以根據具體的情況使用synchronized 關鍵字來修飾一個方法,或者一段代碼。這個方法或者代碼就像是前文中提到的“受保護的廁所,加鎖的廁所”。也可以使用 java 5以后的 Lock 來實現,與 synchronized 關鍵字相比,Lock 的使用更靈活,可以有加鎖超時時間、公平性等優勢。
分布式鎖
上面我們所說的 synchronized 關鍵字也好,Lock 也好。其實他們的作用范圍是啥,就是當前的應用啊。你的代碼在這個 jar 包或者這個 war 包里邊,被部署在 A 機器上。那么實際上我們寫的 synchronized 關鍵字,就是在當前的機器的 JVM在執行代碼的時候發生作用的。假設這個代碼被部署到了三台機器上 A,B,C。那么 A 機器中的部署的代碼中的synchronized 關鍵字並不能控制 B,C 中的內容。
假如我們需要在 A,B,C 三台機器上運行某段程序的時候,實現“原子操作”,synchronized 關鍵字或者 Lock 是不能滿足的。很顯然,這個時候我們需要的鎖,是需要協同這三個節點的,於是,分布式鎖就需要上場了,他就像是在A,B,C的外面加了一個層,通過它來實現鎖的控制。
redis 如何實現加鎖
在redis中,有一條命令,可以實現類似 “鎖” 的語法是這樣的:
SETNX key value
他的作用是,將 key
的值設為 value
,當且僅當 key
不存在。若給定的 key
已經存在,則 SETNX 不做任何動作。設置成功,返回 1
;設置失敗,返回 0
。
使用 redis 來實現鎖的邏輯就是這樣的
線程 1 獲取鎖 -- > setnx mylock lockvalue
-- > 1 獲取鎖成功
線程 2 獲取鎖 -- > setnx mylock lockvalue
-- > 0 獲取鎖失敗 (繼續等待,或者其他邏輯)
線程 1 釋放鎖 -- >
線程 2 獲取鎖 -- > setnx mylock lockvalue
-- > 1 獲取成功
鎖超時
在這個例子中,我們梳理了使用 redis setnx 命令 來實現鎖的邏輯。這里還需要考慮的是,鎖超時的問題 ,因為當線程 1 獲取了鎖之后,如果業務邏輯執行很長很長時間,那么其他線程只能死等,這可不行。所以需要加上超時,結合這些考慮的情況,實際的 Java 代碼可以這樣寫:
public static boolean lock(String key,String lockValue,int expire){
if(null == key){
return false;
}
try {
Jedis jedis = getJedisPool().getResource();
String res = jedis.set(key,lockValue,"NX","EX",expire);
jedis.close();
return res!=null && res.equals("OK");
} catch (Exception e) {
return false;
}
}
retry
這里執行加鎖,不一定能成功。當別人正在持有鎖的時候,加鎖的線程需要繼續嘗試。這個“繼續嘗試”通常是“忙等待”,實現代碼如下:
/**
* 獲取一個分布式鎖 , 超時則返回失敗
* @param key 鎖的key
* @param lockValue 鎖的value
* @param timeout 獲取鎖的等待時間,單位為 秒
* @return 獲鎖成功 - true | 獲鎖失敗 - false
*/
public static boolean tryLock(String key,String lockValue,int timeout,int expire){
final long start = System.currentTimeMillis();
if(timeout > expiredNx) {
timeout = expiredNx;
}
final long end = start + timeout * 1000;
boolean res = false; // 默認返回失敗
while(!(res = lock(key,lockValue,expire))){ // 調用了上面的 lock方法
if(System.currentTimeMillis() > end) {
break;
}
}
return res;
}
redis 如何釋放鎖
根據上面所述,我們在加鎖的時候執行了:setnx mylock lockvalue
, 這種加鎖的本質其實就是 “占座位”,我把一本書放在自習室第一排的第一個座位上,別人就不能坐了,就得等着我走了,把東西拿走了,他就可以使用這個座位了。所以很容易想到,在我們需要釋放鎖的時候,只需要調用 del mylock
就行了,這樣別的線程想去執行加鎖的時候執行就可以執行 setnx mylock lockvalue
了。
不該釋放的鎖
但是,直接執行del mylock
是有問題的,我們不能直接執行 del mylock
為什么?—— 會導致 “信號錯誤”,釋放了不該釋放的鎖 。假設如下場景:
時間線 | 線程1 | 線程2 | 線程3 |
---|---|---|---|
時刻1 | 執行 setnx mylock val1 加鎖 | 執行 setnx mylock val2 加鎖 | 執行 setnx mylock val2 加鎖 |
時刻2 | 加鎖成功 | 加鎖失敗 | 加鎖失敗 |
時刻3 | 執行任務... | 嘗試加鎖... | 嘗試加鎖... |
時刻4 | 任務繼續(鎖超時,自動釋放了) | setnx 獲得了鎖(因為線程1的鎖超時釋放了) | 仍然嘗試加鎖... |
時刻5 | 任務完畢,del mylock 釋放鎖 | 執行任務中... | 獲得了鎖(因為線程1釋放了線程2的) |
... |
上面的表格中,有兩個維度,一個是縱向的時間線,一個是橫線的線程並發競爭。我們可以發現線程 1 在開始的時候比較幸運,獲得了鎖,最先開始執行任務,但是,由於他比較耗時,最后鎖超時自動釋放了他都還沒執行完。 因此,線程 2 和線程3 的機會來了。而這一輪,線程2 比較幸運,得到了鎖。可是,當線程2正在執行任務期間,線程1 執行完了,還把線程2的鎖給釋放了。這就相當於,本來你鎖着門在廁所里邊尿尿,進行到一半的時候,別人進來了,因為他配了一把和你一模一樣的鑰匙!這就亂套了啊
因此,我們需要安全的釋放鎖——“不是我的鎖,我不能瞎釋放”。所以,我們在加鎖的時候,就需要標記“這是我的鎖”,在釋放的時候在判斷 “ 這是不是我的鎖?”。這里就需要在釋放鎖的時候加上邏輯判斷,合理的邏輯應該是這樣的:
1. 線程1 准備釋放鎖 , 鎖的key 為 mylock 鎖的 value 為 thread1_magic_num
2. 查詢當前鎖 current_value = get mylock
3. 判斷 if current_value == thread1_magic_num -- > 是 我(線程1)的鎖
else -- >不是 我(線程1)的鎖
4. 是我的鎖就釋放,否則不能釋放(而是執行自己的其他邏輯)。
為了實現上面這個邏輯,我們是無法通過 redis 自帶的命令直接完成的。如果,再寫復雜的代碼去控制釋放鎖,則會讓整體代碼太過於復雜了。所以,我們引入了lua腳本。結合Lua 腳本實現釋放鎖的功能,更簡單,redis 執行lua腳本也是原子的,所以更合適,讓合適的人干合適的事,豈不更好。
通過Lua腳本實現鎖釋放
Lua是啥,Lua是一種功能強大,高效,輕量級,可嵌入的腳本語言。其官方的描述是:
Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.
Lua 調用 redis 非常簡單,並且 Lua 腳本語法也易學,對於有別的編程語言基礎的程序員來說,在不學習Lua腳本語法的情況下,直接看 Lua 的代碼 也是可以看懂的。例子如下:
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
上面的代碼,邏輯很簡單,if 中的比較如果是true , 那么 執行 del 並返回del結果;如果 if 結果為false 直接返回 0 。這不就滿足了我們釋放鎖的要求嗎?——“ 是我的鎖,我就釋放,不是我的鎖,我不能瞎釋放”。
其中的KEYS[1] , ARGV[1] 是參數,我們只調用 jedis 執行腳本的時候,傳遞這兩個參數就可以了。
使用redis + lua 來實現釋放鎖的代碼如下:
private static final Long lockReleaseOK = 1L;
static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua腳本,用來釋放分布式鎖
public static boolean releaseLock(String key ,String lockValue){
if(key == null || lockValue == null) {
return false;
}
try {
Jedis jedis = getJedisPool().getResource();
Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue));
jedis.close();
return res!=null && res.equals(lockReleaseOK);
} catch (Exception e) {
return false;
}
}
如此,我們便實現了鎖的安全釋放。同時,我們還需要結合業務邏輯,進行具體健壯性的保證,比如如果結束了一定不能忘記釋放鎖,異常了也要釋放鎖,某種情況下是否需要回滾事務等。總結這個分布式鎖使用的過程便是:
- 加鎖時 key 同,value 不同。
- 釋放鎖時,根據value判斷,是不是我的鎖,不能釋放別人的鎖。
- 及時釋放鎖,而不是利用自動超時。
- 鎖超時時間一定要結合業務情況權衡,過長,過短都不行。
- 程序異常之處,要捕獲,並釋放鎖。如果需要回滾的,主動做回滾、補償。保證整體的健壯性,一致性。
用redis做分布式鎖真的靠譜嗎
上面的文字中,我們討論如何使用redis作為分布式鎖,並討論了一些細節問題,如鎖超時的問題、安全釋放鎖的問題。目前為止,似乎很完美的解決的我們想要的分布式鎖功能。然而事情並沒有這么簡單,用redis做分布式鎖並不“靠譜”。
不靠譜的情況
上面我們說的是redis,是單點的情況。如果是在redis sentinel集群中情況就有所不同了。關於redis sentinel 集群可以看這里。在redis sentinel集群中,我們具有多台redis,他們之間有着主從的關系,例如一主二從。我們的set命令對應的數據寫到主庫,然后同步到從庫。當我們申請一個鎖的時候,對應就是一條命令 setnx mykey myvalue
,在redis sentinel集群中,這條命令先是落到了主庫。假設這時主庫down了,而這條數據還沒來得及同步到從庫,sentinel將從庫中的一台選舉為主庫了。這時,我們的新主庫中並沒有mykey這條數據,若此時另外一個client執行 setnx mykey hisvalue
, 也會成功,即也能得到鎖。這就意味着,此時有兩個client獲得了鎖。這不是我們希望看到的,雖然這個情況發生的記錄很小,只會在主從failover的時候才會發生,大多數情況下、大多數系統都可以容忍,但是不是所有的系統都能容忍這種瑕疵。
redlock
為了解決故障轉移情況下的缺陷,Antirez 發明了 Redlock 算法,使用redlock算法,需要多個redis實例,加鎖的時候,它會想多半節點發送 setex mykey myvalue
命令,只要過半節點成功了,那么就算加鎖成功了。釋放鎖的時候需要想所有節點發送del命令。這是一種基於【大多數都同意】的一種機制。感興趣的可以查詢相關資料。在實際工作中使用的時候,我們可以選擇已有的開源實現,python有redlock-py,java 中有Redisson redlock。
redlock確實解決了上面所說的“不靠譜的情況”。但是,它解決問題的同時,也帶來了代價。你需要多個redis實例,你需要引入新的庫 代碼也得調整,性能上也會有損。所以,果然是不存在“完美的解決方案”,我們更需要的是能夠根據實際的情況和條件把問題解決了就好。
至此,我大致講清楚了redis分布式鎖方面的問題(日后如果有新的領悟就繼續更新)。
redis單點、redis主從、redis集群cluster配置搭建與使用
Netty開發redis客戶端,Netty發送redis命令,netty解析redis消息
spring如何啟動的?這里結合spring源碼描述了啟動過程
SpringMVC是怎么工作的,SpringMVC的工作原理
spring 異常處理。結合spring源碼分析400異常處理流程及解決方法
Mybatis Mapper接口是如何找到實現類的-源碼分析
CORS詳解,CORS原理分析
Docker & k8s 系列一:快速上手docker
Docker & k8s 系列二:本機k8s環境搭建
Docker & k8s 系列三:在k8s中部署單個服務實例
Docker & Kubenetes 系列四:集群,擴容,升級,回滾