使用現狀
Redis分佈鎖的基礎內容,我們已經在基於AOP和Redis實現的簡易版分散式鎖這篇文章中講過了,也在文章中示範了正常的加鎖和解鎖方法。
分散式鎖在之前的專案中一直執行良好,沒有辜負我們的期望。
發現問題
但在最近查線上日誌的時候偶然發現,有一個業務場景下,分散式鎖偶爾會失效,導致有多個執行緒同時執行了相同的程式碼。
我們經過初步排查,定位到是因為在這段程式碼中間呼叫了第三方的介面導致。
因為業務程式碼耗時過長,超過了鎖的超時時間,造成鎖自動失效,然後另外一個執行緒意外的持有了鎖。於是就出現了多個執行緒共同持有鎖的現象。
解決方案
問題既然已經出現了,那麼接下來我們就應該考慮解決方案了。
我們也曾經想過,是否可以通過合理地設定LockTime(鎖超時時間)來解決這個問題?
但LockTime的設定原本就很不容易。LockTime設定過小,鎖自動超時的概率就會增加,鎖異常失效的概率也就會增加,而LockTime設定過大,萬一服務出現異常無法正常釋放鎖,那麼出現這種異常鎖的時間也就越長。我們只能通過經驗去配置,一個可以接受的值,基本上是這個服務歷史上的平均耗時再增加一定的buff。
既然這條路走不通了,那麼還有其他路可以走麼?
當然還是有的,我們可以先給鎖設定一個LockTime,然後啟動一個守護執行緒,讓守護執行緒在一段時間後,重新去設定這個鎖的LockTime。
看起來很簡單是不是?
但在實際操作中,我們要注意以下幾點:
1、和釋放鎖的情況一致,我們需要先判斷鎖的物件是否沒有變。否則會造成無論誰持有鎖,守護執行緒都會去重新設定鎖的LockTime。不應該續的不能瞎續。
2、守護執行緒要在合理的時間再去重新設定鎖的LockTime,否則會造成資源的浪費。不能動不動就去續。
3、如果持有鎖的執行緒已經處理完業務了,那麼守護執行緒也應該被銷燬。不能主人都掛了,守護者還在那裡繼續浪費資源。
程式碼實現
我們首先先生成一個內部類去實現Runnable,作為守護執行緒的引數。
public class SurvivalClamProcessor implements Runnable {
private static final int REDIS_EXPIRE_SUCCESS = 1;
SurvivalClamProcessor(String field, String key, String value, int lockTime) {
this.field = field;
this.key = key;
this.value = value;
this.lockTime = lockTime;
this.signal = Boolean.TRUE;
}
private String field;
private String key;
private String value;
private int lockTime;
//執行緒關閉的標記
private volatile Boolean signal;
void stop() {
this.signal = Boolean.FALSE;
}
@Override
public void run() {
int waitTime = lockTime * 1000 * 2 / 3;
while (signal) {
try {
Thread.sleep(waitTime);
if (cacheUtils.expandLockTime(field, key, value, lockTime) == REDIS_EXPIRE_SUCCESS) {
if (logger.isInfoEnabled()) {
logger.info("expandLockTime 成功,本次等待{}ms,將重置鎖超時時間重置為{}s,其中field為{},key為{}", waitTime, lockTime, field, key);
}
} else {
if (logger.isInfoEnabled()) {
logger.info("expandLockTime 失敗,將導致SurvivalClamConsumer中斷");
}
this.stop();
}
} catch (InterruptedException e) {
if (logger.isInfoEnabled()) {
logger.info("SurvivalClamProcessor 處理執行緒被強制中斷");
}
} catch (Exception e) {
logger.error("SurvivalClamProcessor run error", e);
}
}
if (logger.isInfoEnabled()) {
logger.info("SurvivalClamProcessor 處理執行緒已停止");
}
}
}
複製程式碼
其中expandLockTime是通過Lua指令碼實現的。延長鎖超時的指令碼語句和釋放鎖的Lua指令碼類似。
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1],ARGV[2]) else return '0' end";
複製程式碼
在以上程式碼中,我們將waitTime設定為Math.max(1, lockTime * 2 / 3),即守護執行緒許需要等待waitTime後才可以去重新設定鎖的超時時間,避免了資源的浪費。
同時在expandLockTime時候也去判斷了當前持有鎖的物件是否一致,避免了胡亂重置鎖超時時間的情況。
然後我們在獲得鎖的程式碼之後,新增如下程式碼:
SurvivalClamProcessor survivalClamProcessor
= new SurvivalClamProcessor(lockField, lockKey, randomValue, lockTime);
Thread survivalThread = new Thread(survivalClamProcessor);
survivalThread.setDaemon(Boolean.TRUE);
survivalThread.start();
Object returnObject = joinPoint.proceed(args);
survivalClamProcessor.stop();
survivalThread.interrupt();
return returnObject;
複製程式碼
這段程式碼會先初始化守護執行緒的內部引數,然後通過start函式啟動執行緒,最後在業務執行完之後,設定守護執行緒的關閉標記,最後通過interrupt()去中斷sleep狀態,保證執行緒及時銷燬。
後續
本文講解了如何通過啟動一個守護執行緒去重置鎖超時時間,也同時介紹了在實現過程的注意點。隨帶著也科普了一下執行緒銷燬的正確方式。
那麼關於分散式鎖還有下文麼?我也不知道,權當是有吧,可能下一期會講講如何通過其他方式(除Redis之外的)去實現分散式鎖,也可能是講一下Redis分散式鎖的其他問題和解決方案。
好了,我們下一期再見,歡迎大家一起留言討論。同時也歡迎點贊,歡迎送小星星~