《原神攻略》茂知之殼祕境怎麼解鎖?茂知之殼解鎖方法
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
:鎖的標識requestId
:請求idNX
:只在鍵不存在時,才對鍵進行設定操作。PX
:設定鍵的過期時間為 millisecond 毫秒。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)
這三個要素最多隻能同時實現兩點,不可能三者兼顧。
如果你的實際業務場景,更需要的是保證資料一致性。那麼請使用CP型別的分散式鎖,比如:zookeeper,它是基於磁碟的,效能可能沒那麼好,但資料一般不會丟。
如果你的實際業務場景,更需要的是保證資料高可用性。那麼請使用AP型別的分散式鎖,比如:redis,它是基於記憶體的,效能比較好,但有丟失資料的風險。