1. 程式人生 > 遊戲攻略 >《原神攻略》茂知之殼祕境怎麼解鎖?茂知之殼解鎖方法

《原神攻略》茂知之殼祕境怎麼解鎖?茂知之殼解鎖方法

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:請求id
  • NX:只在鍵不存在時,才對鍵進行設定操作。
  • PX:設定鍵的過期時間為 millisecond 毫秒。
  • expireTime:過期時間

分散式鎖的合理使用方式:

  1. 手動加鎖
  2. 業務操作
  3. 手動釋放鎖
  4. 如果手動釋放鎖失敗了,則達到超時時間,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
  1. 先判斷如果鎖名不存在,則加鎖。
  2. 接下來,判斷如果鎖名和requestId值都存在,則使用hincrby命令給該鎖名和requestId值計數,每次都加1。注意一下,這裡就是重入鎖的關鍵,鎖重入一次值就加1。
  3. 如果鎖名存在,但值不是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
  1. 先判斷如果鎖名和requestId值不存在,則直接返回。
  2. 如果鎖名和requestId值存在,則重入鎖減1。
  3. 如果減1後,重入鎖的value值還大於0,說明還有引用,則重試設定過期時間。
  4. 如果減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解決問題的思路如下:

  1. 需要搭建幾套相互獨立的redis環境,假如我們在這裡搭建了5套。
  2. 每套環境都有一個redisson node節點。
  3. 多個redisson node節點組成了RedissonRedLock。
  4. 環境包含:單機、主從、哨兵和叢集模式,可以是一種或者多種混合。

RedissonRedLock加鎖過程如下:

  1. 獲取所有的redisson node節點資訊,迴圈向所有的redisson node節點加鎖,假設節點數為N,例子中N等於5。
  2. 如果在N個節點當中,有N/2 + 1個節點加鎖成功了,那麼整個RedissonRedLock加鎖是成功的。
  3. 如果在N個節點當中,小於N/2 + 1個節點加鎖成功,那麼整個RedissonRedLock加鎖是失敗的。
  4. 如果中途發現各個節點加鎖的總耗時,大於等於設定的最大等待時間,則直接返回失敗。

不過也引出了一些新的問題:

  1. 要額外搭建多套環境,申請更多的資源,需要評估一下成本和價效比。
  2. 如果有N個redisson node節點,需要加鎖N次,最少也需要加鎖N/2+1次,才知道redlock加鎖是否成功。顯然,增加了額外的時間成本,有點得不償失。

在分散式環境中,CAP是繞不過去的。

CAP指的是在一個分散式系統中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分割槽容錯性(Partition tolerance)

這三個要素最多隻能同時實現兩點,不可能三者兼顧。

如果你的實際業務場景,更需要的是保證資料一致性。那麼請使用CP型別的分散式鎖,比如:zookeeper,它是基於磁碟的,效能可能沒那麼好,但資料一般不會丟。

如果你的實際業務場景,更需要的是保證資料高可用性。那麼請使用AP型別的分散式鎖,比如:redis,它是基於記憶體的,效能比較好,但有丟失資料的風險。