Redis分布式鎖
在分布式系統中,由于redis分布式鎖相對于更簡單和高效,成為了分布式鎖的首先,被我們用到了很多實際業務場景當中。
Redis分布式鎖常見問題:
- 非原子操作
- 忘記釋放鎖
- 釋放了其他人的鎖
- 大量失敗請求
- 鎖重入問題
- 鎖競争問題
- 鎖逾時問題
- 主從複制問題
加鎖:
// 此方式setNx指令設定鎖和設定逾時時間是分開的,非原子操作
if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}
// 使用set指令結合多個參數,該操作為原子操作
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
其中:
-
:鎖的辨別lockKey
-
:請求idrequestId
-
:隻在鍵不存在時,才對鍵進行設定操作。NX
-
:設定鍵的過期時間為 millisecond 毫秒。PX
-
:過期時間expireTime
分布式鎖的合理使用方式:
- 手動加鎖
- 業務操作
- 手動釋放鎖
- 如果手動釋放鎖失敗了,則達到逾時時間,redis會自動釋放鎖。
釋放鎖
// 在finally塊裡釋放鎖,即使因系統當機鎖也會因設定的逾時時間而釋放
try{
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
} finally {
unlock(lockKey);
}
但仍可能會出現釋放了别人的鎖的問題:
假如線程A和線程B,都使用lockKey加鎖。線程A加鎖成功了,但是由于業務功能耗時時間很長,超過了設定的逾時時間。這時候,redis會自動釋放lockKey鎖。此時,線程B就能給lockKey加鎖成功了,接下來執行它的業務操作。恰好這個時候,線程A執行完了業務功能,接下來,在finally方法中釋放了鎖lockKey。這不就出問題了,線程B的鎖,被線程A釋放了。
解決方案:根據業務場景确定requestId,使用requestId來設定lockKey.(自己隻能釋放自己的鎖)
lua腳本加鎖操作:
// redisson架構加鎖代碼:
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)
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:秒殺場景:每1W個請求,有1個成功,再1W個請求,有1個成功;(不合理,合理場景應該是:1W個請求,成功1個,失敗的部分應繼續參與競争)
解決方案:自旋鎖,失敗後休眠一段時間繼續發起新一輪嘗試(根據業務場景設定休眠時間和嘗試次數)
遞歸加鎖場景中的問題需使用可重入鎖解決
// redisson可重入鎖使用僞代碼
private int expireTime = 1000;
public void run(String lockKey) {
RLock lock = redisson.getLock(lockKey);
this.fun(lock,1);
}
public void fun(RLock lock,int level){
try{
lock.lock(5, TimeUnit.SECONDS);
if(level<=10){
this.fun(lock,++level);
} else {
return;
}
} finally {
lock.unlock();
}
}
redisson可重入鎖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]);
- KEYS[1]:鎖名
- ARGV[1]:過期時間
- ARGV[2]:uuid + ":" + threadId,可認為是requestId
- 先判斷如果鎖名不存在,則加鎖。
- 接下來,判斷如果鎖名和requestId值都存在,則使用hincrby指令給該鎖名和requestId值計數,每次都加1。注意一下,這裡就是重入鎖的關鍵,鎖重入一次值就加1。
- 如果鎖名存在,但值不是requestId,則傳回過期時間。
redisson釋放鎖lua腳本:
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
- 先判斷如果鎖名和requestId值不存在,則直接傳回。
- 如果鎖名和requestId值存在,則重入鎖減1。
- 如果減1後,重入鎖的value值還大于0,說明還有引用,則重試設定過期時間。
- 如果減1後,重入鎖的value值還等于0,則可以删除鎖,然後發消息通知等待線程搶鎖。
通過控制鎖的粒度來提升redis分布式鎖性能:讀寫鎖,鎖分段
redisson中的讀寫鎖示例:
// 讀鎖
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
//業務操作
} catch (Exception e) {
log.error(e);
} finally {
rLock.unlock();
}
// 寫鎖
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {
rLock.lock();
//業務操作
} catch (InterruptedException e) {
log.error(e);
} finally {
rLock.unlock();
}
線程A擷取鎖執行業務由于耗時過多導緻逾時釋放了鎖,線程B開始執行,此時線程A仍在執行,會導緻意想不到的情況
解決方案:鎖在達到逾時時間後需要給鎖自動續期
// 可以使用TimerTask類來實作自動續期
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//自動續期邏輯
}
}, 10000, TimeUnit.MILLISECONDS);
擷取鎖之後,自動開啟一個定時任務,每隔10秒鐘,自動重新整理一次過期時間。這種機制在redisson架構中,有個比較霸氣的名字:
watch dog
,即傳說中的
看門狗
。
自動續期操作的lua腳本實作:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
需要注意的地方是:在實作自動續期功能時,還需要設定一個總的過期時間,可以跟redisson保持一緻,設定成30秒。如果業務代碼到了這個總的過期時間,還沒有執行完,就不再自動續期了。
對于哨兵模式的redis使用分布式鎖問題:
剛加上鎖後master節點還未來得及同步到從節點就挂了
redisson架構為了解決這個問題,提供了一個專門的類:
RedissonRedLock
,使用了Redlock算法。
RedissonRedLock解決問題的思路如下:
- 需要搭建幾套互相獨立的redis環境,假如我們在這裡搭建了5套。
- 每套環境都有一個redisson node節點。
- 多個redisson node節點組成了RedissonRedLock。
- 環境包含:單機、主從、哨兵和叢集模式,可以是一種或者多種混合。
RedissonRedLock加鎖過程如下:
- 擷取所有的redisson node節點資訊,循環向所有的redisson node節點加鎖,假設節點數為N,例子中N等于5。
- 如果在N個節點當中,有N/2 + 1個節點加鎖成功了,那麼整個RedissonRedLock加鎖是成功的。
- 如果在N個節點當中,小于N/2 + 1個節點加鎖成功,那麼整個RedissonRedLock加鎖是失敗的。
- 如果中途發現各個節點加鎖的總耗時,大于等于設定的最大等待時間,則直接傳回失敗。
不過也引出了一些新的問題:
- 要額外搭建多套環境,申請更多的資源,需要評估一下成本和成本效益。
- 如果有N個redisson node節點,需要加鎖N次,最少也需要加鎖N/2+1次,才知道redlock加鎖是否成功。顯然,增加了額外的時間成本,有點得不償失。
在分布式環境中,CAP是繞不過去的。
CAP指的是在一個分布式系統中:
- 一緻性(Consistency)
- 可用性(Availability)
- 分區容錯性(Partition tolerance)