①演進階段一
獲得鎖就執行業務邏輯,沒有獲得鎖就繼續調用這個方法形成一個自旋,就類似於synchronized
。
僞代碼:
public void getData(){
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
if(lock){
// 執行業務..
// 刪除鎖
redisTemplate.delete("lock");
}else{
// 休眠一段時間
// 繼續調用getData,等待鎖的釋放
getData();
}
}
存在問題:
setnx佔好了位,業務代碼異常或者程序在頁面過程中宕機。沒有執行刪除鎖邏輯,這就造成了死鎖
。
解決方法:
給鎖設置一個過期時間
,即使代碼異常或是程序宕機,鎖都會因爲過期時間到了自動刪除。
②演進階段二
解決階段一的問題。
僞代碼:
public void getData(){
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
if(lock){
// 設置過期時間
redisTemplate.expire("lock",180,TimeUnit.SECONDS);
// 執行業務..
// 刪除鎖
redisTemplate.delete("lock");
}else{
// 休眠一段時間
// 繼續調用getData,等待鎖的釋放
getData();
}
}
存在問題:
獲得鎖之後,正要去設置過期時間,這是服務宕機/斷電,這個時間還沒設置上去,又導致了死鎖。
解決方法:
這就需要保證設置value和過期時間是原子性操作,這就需要使用Redis的setnx ex命令。
原來將鍵key設定爲指定的“字符串”值,如果 key 已經保存了一個值,那麼這個操作會直接覆蓋原來的值,並且忽略原始類型。
在2.6.12版本開始,redis爲SET命令增加了一系列選項:
EX seconds 設置鍵key的過期時間,單位時秒
PX milliseconds 設置鍵key的過期時間,單位時毫秒
NX 只有鍵key不存在的時候纔會設置key的值
XX 只有鍵key存在的時候纔會設置key的值
命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
③演進階段三
解決階段二的問題。
僞代碼:
public void getData(){
// 設置了過期時間 - 180s
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
if(lock){
// 執行業務...
// 刪除鎖
redisTemplate.delete("lock");
}else{
// 休眠一段時間
// 繼續調用getData,等待鎖的釋放
}
}
存在問題:
如果由於業務時間很長,鎖自己過期了,我們直接刪除,有可能把別人正在持有的鎖刪除了。
場景:
比如我們執行一個業務,線程1拿到了Redis鎖,Redis鎖過期時間是180s,我們這個業務卻執行了300s,也就是說,我們執行到180s的時候,鎖已經過期了,那另外一個線程2拿到了鎖,等我們線程1業務執行完的時候,線程2業務還在執行中,線程1要去刪鎖,這時刪除的鎖其實是線程2的。
解決方法:
使用隨機的大字符串作爲value值,刪除前先對比value值是否相同,相同就刪除。
④演進階段四
解決階段三的問題。
僞代碼:
public void getData(){
// 使用UUID生成不重複值
String uuid = UUID.randomUUID().toString();
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
// 執行業務...
// 判斷uuid是否相同
String str = redisTemplate.get("lock");
if(uuid.equals(str){ // 相同,就刪除鎖
redisTemplate.delete("lock");
}
}else{
// 休眠一段時間
// 繼續調用getData,等待鎖的釋放
}
}
解決方法:
保證對比value值和刪除value值是一個原子性操作,使用Redis+Lua腳本來完成。
⑤演進階段五
解決階段四的問題,最終形態。
僞代碼:
public void getData(){
// 使用UUID生成不重複值
String uuid = UUID.randomUUID().toString();
boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
if(lock){
try{
// 執行業務...
}finally{
// Lua腳本 - 2個參數 key和value
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
}
}else{
// 休眠一段時間
// 繼續調用getData,等待鎖的釋放
}
}
存在問題:
鎖的過期時間,如果業務沒執行完,鎖應該續期,最簡單的解決方法就是把鎖的過期時間設置大一點。
分佈式鎖框架
分佈式鎖框架Redisson,基於Redis實現的各種分佈式鎖。
Redisson的GitHub地址