99热99这里只有精品6国产,亚洲中文字幕在线天天更新,在线观看亚洲精品国产福利片 ,久久久久综合网

歡迎加入QQ討論群258996829
麥子學院 頭像
蘋果6袋
6
麥子學院

如何基于redis在分布式環(huán)境下實現(xiàn)一個全局鎖?

發(fā)布時間:2016-11-25 18:51  回復:0  查看:2889   最后回復:2016-11-25 18:51  

今天和大家分享的是redis學習中,如何在分布式環(huán)境下實現(xiàn)一個全局鎖,在開始之前先說說非分布式下的鎖:

  · 單機 – 單進程程序使用互斥鎖mutex,解決多個線程之間的同步問題

  · 單機 – 多進程程序使用信號量sem,解決多個進程之間的同步問題

  這里同步的意思很簡單:某個運行者,用某個工具,保障某段代碼,獨占的運行,直到釋放。

  分布式鎖解決的是 多臺機器 – 多個進程 之間的同步問題,因為不同的機器之間mutex/sem無法使用。不過要注意:即便如此,一個進程內(nèi)多個線程之間仍舊建議使用mutex同步,盡量減少對分布式鎖服務造成不必要的負擔。

  redis分布式鎖

  首先呢,基于redis的分布式鎖并不是一個坊間方案,而是redis官網(wǎng)提供的解決思路并且有若干語言的實現(xiàn)版本直接使用。

  今天要做的,首先是閱讀官方的文檔,有些地方講的不怎么清晰,所以我接下來會分析PHP版本的代碼,應該可以解答你的主要疑惑。

  分析代碼

  構造函數(shù)

  function __construct(array $servers, $retryDelay = 200, $retryCount = 3)

  {

  $this->servers = $servers;

  $this->retryDelay = $retryDelay;

  $this->retryCount = $retryCount;

  $this->quorum = min(count($servers), (count($servers) / 2 + 1));

  }

  · 需要傳入的是redis的若干master節(jié)點地址,并且這些master是純內(nèi)存模式且無slave的。

  · retryDelay是設置每一輪lock失敗或者異常,多久之后重新嘗試下一輪lock

  · retryCount是指最多幾輪lock失敗后徹底放棄。

  · quorum體現(xiàn)了分布式里一個有名的鴿巢原理,也就是如果大于半數(shù)的節(jié)點操作成功則認為整個集群是操作成功的;在這里的意思是,如果超過1/2(>=N/2+1)redis master調(diào)用鎖成功,則認為獲得了整個redis集群的鎖,假設A用戶獲得了集群的鎖,那么接下來的B用戶只能獲得<=1/2redis master的鎖,相當于無法獲得集群的鎖。

  初始化redis連接

  private function initInstances()

  {

  if (empty($this->instances)) {

  foreach ($this->serversas $server) {

  list($host, $port, $timeout) = $server;

  $redis = new \Redis();

  $redis->connect($host, $port, $timeout);

  $this->instances[] = $redis;

  }

  }

  }

  · 遍歷每個redis master,建立到它們的連接并保存起來;

  · 因為需要用到鴿巢原理,也就是redis數(shù)量足夠產(chǎn)生大多數(shù)這個目的:因此redis master數(shù)量最好>=3臺,因為2臺的話大多數(shù)是2(2/2+1),這樣任何1臺故障就無法產(chǎn)生大多數(shù),那么整個分布式鎖就不可用了。

  請求1redis上鎖

  private function lockInstance($instance, $resource, $token, $ttl)

  {

  return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);

  }

  · 請求某一臺redis,如果key=resource不存在就設置value=token(算法生成,全局唯一),并且redis會在ttl時間后自動刪除這個key

  請求1redis放鎖

  private function unlockInstance($instance, $resource, $token)

  {

  $script = '

  if redis.call("GET", KEYS[1]) == ARGV[1] then

  return redis.call("DEL", KEYS[1])

  else

  return 0

  end

  ';

  return $instance->eval($script, [$resource, $token], 1);

  }

  · 請求某一臺redis,給它發(fā)送一段lua腳本,如果resourcevalue不等于lock時設置的token則說明鎖已被它人占用無需釋放,否則說明是自己上的鎖可以DEL刪除。

  · lua腳本在redis里原子執(zhí)行,在這里即保障GETDEL的原子性。

  請求集群鎖

  public function lock($resource, $ttl)

  {

  $this->initInstances();

  $token = uniqid();

  $retry = $this->retryCount;

  do {

  $n = 0;

  $startTime = microtime(true) * 1000;

  foreach ($this->instancesas $instance) {

  if ($this->lockInstance($instance, $resource, $token, $ttl)) {

  $n++;

  }

  }

  # Add 2 milliseconds to the drift to account for Redis expires

  # precision, which is 1 millisecond, plus 1 millisecond min drift

  # for small TTLs.

  $drift = ($ttl * $this->clockDriftFactor) + 2;

  $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;

  if ($n >= $this->quorum && $validityTime > 0) {

  return [

  'validity' => $validityTime,

  'resource' => $resource,

  'token' => $token,

  ];

  else {

  foreach ($this->instancesas $instance) {

  $this->unlockInstance($instance, $resource, $token);

  }

  }

  // Wait a random delay before to retry

  $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);

  usleep($delay * 1000);

  $retry--;

  while ($retry > 0);

  return false;

  }

  · 首先整個lock過程最多會重試retry次,因此外層有do while。

  · 為了獲取大多數(shù)的鎖,因此遍歷每個redis masterlock,統(tǒng)計成功的次數(shù)。

  · 因為遍歷redis master進行逐個上鎖需要花費一定的時間,因此在第1redis上鎖前記錄時間T1,結束最后一個redis上鎖動作的時間點T2,此時第1redisTTL已經(jīng)消逝了T2-T1這么長的時間。

  · 為了保障在鎖內(nèi)計算期間鎖不會失效,我們剩余可以占用鎖的時間實際上是TTL – (T2 – T1),因為越靠前上鎖的redis其剩余時間越少,最少的就是第1redis了。

  · drift值用于補償不同機器時鐘的精度差異,怎么理解呢:

  · 在我們的程序看來時間過去了(T2-T1),剩余的鎖時間認為是TTL-(T2-T1),在接下來的剩余時間內(nèi)進行計算應該不會超過鎖的有效期。

  · 但是第1redis機器的機器時鐘也許跑的比較快(比如時鐘多前進了1毫秒),那么數(shù)據(jù)會提前1毫秒淘汰,然而我們認為TTL-(T2-T1)秒內(nèi)鎖有效,而redis相當于TTL-(T2-T1)-1秒內(nèi)鎖有效,這可能導致我們在鎖外計算。(drift+1

  · 另外,我們計算(T2-T1)之后到返回給lock的調(diào)用者之間還有一段代碼在運行,這段代碼的花費也將占用一些時間,所以drift應該也考慮這個。(drift+1

  · 最后,ttl * 0.01的意思是ttl越長,那么時鐘可能差異越大,所以這里做了一個動態(tài)計算的補償,比如ttl=100ms,那么就補償1ms的時鐘誤差,盡量避免遇到鎖已過期而我們?nèi)耘f在計算的情況發(fā)生。

  · 如果鎖redis成功的次數(shù)>1/2,并且整個遍歷redis+鎖定的過程的耗時 沒有超過鎖的有效期,那么lock成功,將剩余的鎖時間(TTL減去上鎖花費的時間)鎖的標識token 返回給用戶。

  · 如果上鎖中途失?。ǚ祷?/span>key已存在)或者異常(不知道操作結果),那么都認為上鎖失??;如果上鎖失敗的數(shù)量超過1/2,那么本次上鎖失敗,需要遍歷所有redis進行回滾(回滾失敗也沒有辦法,其他人只能等待我們的key過期,并不會有什么錯誤)。

  釋放集群鎖

  public function unlock(array $lock)

  {

  $this->initInstances();

  $resource = $lock['resource'];

  $token = $lock['token'];

  foreach ($this->instancesas $instance) {

  $this->unlockInstance($instance, $resource, $token);

  }

  }

  · 遍歷所有redis,利用lua腳本原子的安全的釋放自己建立的鎖。

  故障處理

  這里所有redis都是master,不開啟持久化,也不需要slave

  如果某臺redis宕機, 那么不要立即重啟它 ,因為宕機后redis沒有任何數(shù)據(jù),如果你此時重啟它,那么其他進程就可以可以鎖住一個本應還沒有過期的key,這可能導致2個調(diào)用者同時在鎖內(nèi)進行計算,舉個例子吧:

  3redis,兩個用戶AB,有這么1個典型流程來說明上述情況:

  · A發(fā)起lock,鎖住了2redis(r1+r2),超過3/2+1(大多數(shù)),開始執(zhí)行鎖內(nèi)操作。

  · r0() r1(A) r2(A

  · r1宕機,立即重啟,數(shù)據(jù)全部丟失;A仍舊在進行鎖內(nèi)計算,并不知情。

  · r0() r1() r2(A)

  · B發(fā)起lock,鎖住了2redis(r0+r1),超過3/2+1(大多數(shù)),開始執(zhí)行鎖內(nèi)操作。

  · r0(B) r1(B) r2(A)

  悲劇的事情發(fā)生了,因為r1宕機立即重啟導致B可以成功鎖住大多數(shù)”redis,導致AB并發(fā)操作。

  紅色字體就是解決這個問題的:不要立即重啟,保持r1無法聯(lián)通,這樣的話B只能鎖住r0,沒有達到大多數(shù)從而上鎖失敗。那么何時重啟r1呢?根據(jù)業(yè)務最大的TTL判斷,當過了TTL秒后redis中所有key都會過期,遵守規(guī)則的A用戶的計算也應早已結束,此時B獲得鎖也可以保證獨占。

  當然,無論宕機幾臺原理都是一樣的,不要立即重啟,等待最大TTL過期后再啟動redis,你可以自己分析上述例子,假設r0r1一起宕機看看又會發(fā)生什么。

  分布式鎖用途

  我也沒有經(jīng)驗,不過猜想一個場景:

  庫存服務通常需要高并發(fā)的update一行記錄以更新商品的剩余數(shù)量,而我們知道mysqlupdate是行鎖的,如果并發(fā)過高造成mysql的工作線程都在等待行鎖,將會影響mysql處理其他請求。

  如果可以把行鎖用redis鎖取代,那么到達mysql層的并發(fā)將永遠都是1,問題將得到解決,不過要注意上述redis鎖的實現(xiàn)有一個問題就是高并發(fā)場景下,可能導致誰都無法獲取大多數(shù)的鎖,不過好在redis一般足夠穩(wěn)定并且上述實現(xiàn)在lock失敗重試時有一個隨機的間隔值,從而讓某個Lock調(diào)用者有機會獲得大多數(shù)。

 

來源:魚兒的博客

您還未登錄,請先登錄

熱門帖子

最新帖子

?