redis 分布式阻塞鎖的實(shí)現(xiàn)(非爭(zhēng)搶右蕊、同步隊(duì)列機(jī)制)

提示:可跳過(guò)背景信息,直接跳到標(biāo)題三閱讀

一. 分布式鎖使用場(chǎng)景

在服務(wù)器后端程序開(kāi)發(fā)中脯丝,分布式鎖主要用于多臺(tái)機(jī)器的多個(gè)進(jìn)程/線程的并發(fā)執(zhí)行問(wèn)題(處理同一數(shù)據(jù))商膊。比如同時(shí)用戶下單時(shí)多個(gè)并發(fā)請(qǐng)求,進(jìn)行扣減同一商品庫(kù)存操作宠进。

并發(fā)執(zhí)行偽代碼
----------
//1.獲取商品庫(kù)存數(shù)量
$num = getNum($pruduct_id)
//2.庫(kù)存相關(guān)邏輯
if ($num < 10) {
   //商品購(gòu)買失敗
   return false;
}
//3.扣減庫(kù)存
setNum($num - 1);
----------

上邊偽代碼在并發(fā)執(zhí)行的時(shí)候晕拆,先getNum、再setNum材蹬,這并非一個(gè)原子操作实幕,會(huì)出現(xiàn)同時(shí)獲取到的庫(kù)存數(shù)量都滿足要求,然后都進(jìn)行減庫(kù)存的情況堤器。

二. 并發(fā)問(wèn)題解決方案

本質(zhì)上的解決思路是昆庇,把多個(gè)異步并發(fā)執(zhí)行的請(qǐng)求變?yōu)橥桨错樞驁?zhí)行。

1. 在數(shù)據(jù)庫(kù)層面處理

加鎖將查詢和修改兩條語(yǔ)句合為一個(gè)原子操作闸溃,比如mysql的select ... for update語(yǔ)句整吆。

2. 在應(yīng)用程序?qū)用嫣幚?php/java/go)

一般的,有以下兩種方案:

  1. 排隊(duì)機(jī)制(異步消息隊(duì)列方案)辉川。將并發(fā)的請(qǐng)求順序入消息隊(duì)列表蝙,然后開(kāi)起一個(gè)單獨(dú)進(jìn)程,逐個(gè)消費(fèi)隊(duì)列內(nèi)容乓旗。
并發(fā)執(zhí)行偽代碼
----------
pushMes(list,'商品1扣減庫(kù)存');
return '商品購(gòu)買中'
----------

單進(jìn)程異步去消費(fèi)隊(duì)列
---------
while(PopMes(list))
{
   //1.獲取商品庫(kù)存數(shù)量
   $num = getNum($pruduct_id);
   //2.庫(kù)存相關(guān)邏輯
   if ($num < 10) {
      //商品購(gòu)買失敗
      return false;
   }
   //3.扣減庫(kù)存
   setNum($num - 1);
   
   //通知購(gòu)買情況
   notify();
}
---------
  1. 爭(zhēng)搶鎖機(jī)制府蛇。多個(gè)請(qǐng)求同時(shí)爭(zhēng)搶一個(gè)分布式的鎖,拿到鎖的請(qǐng)求執(zhí)行完成后釋放鎖屿愚,未拿到鎖的請(qǐng)求循環(huán)sleep一段時(shí)間汇跨,去等待鎖釋放、爭(zhēng)搶鎖妆距。
并發(fā)執(zhí)行偽代碼
----------
times = 0;
while(times < 10) {
   //獲取鎖
   if (getLock()) {
      //1.獲取商品庫(kù)存數(shù)量
      $num = getNum($pruduct_id);
      //2.庫(kù)存相關(guān)邏輯
      if ($num < 10) {
         //商品購(gòu)買失敗
         return false;
      }
      //3.扣減庫(kù)存
      setNum($num - 1);
      
      //釋放鎖
      releaseLock();
      return true;
   } else {
      times = times + 1;
      //等待一段時(shí)間
      sleep(0.01);
   }
}
----------

三. 本文新方案穷遂,分布式非爭(zhēng)搶阻塞鎖(同步隊(duì)列機(jī)制)

1. 概念解讀

  • 首先鎖是分布式的
  • 阻塞鎖指的是,不能拿到鎖的時(shí)候毅厚,會(huì)阻塞程序的執(zhí)行直至拿到鎖
  • 非爭(zhēng)搶指的是塞颁,等待拿鎖的過(guò)程是不用爭(zhēng)搶的,通過(guò)同步隊(duì)列實(shí)現(xiàn)(相對(duì)異步消息隊(duì)列而言)

2. 實(shí)現(xiàn)原理

  • 分布式:創(chuàng)建一個(gè)redis隊(duì)列來(lái)存儲(chǔ)一個(gè)key,作為一個(gè)可用鎖胃惜。
  • 阻塞非爭(zhēng)搶拿鎖:通過(guò)redis的brpop命令來(lái)阻塞獲取一個(gè)鎖
  • 釋放鎖:拿到鎖執(zhí)行完對(duì)應(yīng)業(yè)務(wù)后吼虎,將鎖資源存入redis隊(duì)列


BRPOP 是一個(gè)阻塞的列表彈出原語(yǔ)。 它是 RPOP的阻塞版本贪婉,因?yàn)檫@個(gè)命令會(huì)在給定list無(wú)法彈出任何元素的時(shí)候阻塞連接。關(guān)于redis brpop的非爭(zhēng)搶和阻塞特性的實(shí)現(xiàn),在后邊的文章分析妆棒。

3. 代碼實(shí)現(xiàn)(php)

注: 下面代碼僅為事例代碼,具體應(yīng)用還要考慮其他問(wèn)題。比如加鎖后程序異常退出糕珊,釋放鎖失效的問(wèn)題动分。

<?php

   $redis = new Redis();
   $redis->connect('127.0.0.1', 6379);

   //商品id
   $pruduct_id = 1;
   //鎖的名稱
   $lock_key = 'lock_' . $pruduct_id;
   //產(chǎn)品庫(kù)存在redis中存儲(chǔ)的key
   $store_key = 'product_' . $pruduct_id;
   //初始設(shè)置商品庫(kù)存為2000
   $redis->setnx($store_key, 2000);
   //獲取鎖最多阻塞10s
   $lock = getLock($lock_key, 10);
   //記錄請(qǐng)求數(shù)量
   $redis->incr('request_num');
   if ($lock) {
      $num = $redis->get($store_key);
      if (is_numeric($num) && $num > 10) {
         //減庫(kù)存
         $num--;
         $redis->set($store_key, $num);
      }
      //釋放鎖資源
      releaseLock($lock_key);
   }

   /**
   * 阻塞非爭(zhēng)搶獲取一個(gè)鎖
   * @param string $key 鎖的名稱
   * @param string $timeout 最大阻塞時(shí)間(秒),超過(guò)時(shí)間將不再等待拿鎖
   * @return bool 獲取鎖成功/失敗
   */
   function getLock($key = 'lock1', $timeout) 
   {
      global $redis;
      //第一次請(qǐng)求, 鎖標(biāo)識(shí)不存在的情況红选,直接拿到鎖
      $lock =  $redis->setnx($key, 1);
      if (!$lock) {
        //非第一次請(qǐng)求澜公,阻塞等待拿到鎖
        $lock = $redis->brpop($key . '_list', $timeout);
      }
     return (bool)$lock;
   }

   /**
   * 爭(zhēng)搶獲取一個(gè)鎖(使用setnx實(shí)現(xiàn) 拿不到鎖最多重試100次)
   * @param string $key 鎖的名稱
   * @param string $timeout 最大阻塞時(shí)間(秒),超過(guò)時(shí)間將不再等待拿鎖
   * @return bool 獲取鎖成功/失敗
   */
   function getLock2($key = 'lock1') 
   {
      global $redis;
      $lock =  $redis->setnx($key, 1);
      if (!$lock) {
        for ($i=0; $i < 100; $i++) {
          //記錄拿鎖重試次數(shù)
          $redis->incr('retry');
          usleep(1);
          if ($redis->setnx($key, 1)) {
            return true;
          }
        }
        //記錄拿鎖失敗次數(shù)
        $redis->incr('get_lock_fail');
      }
     return (bool)$lock;
   }

   /**
     * 釋放鎖
     * @param string $key 鎖的名稱
     * @return bool 釋放鎖成功/失敗
     */
   function releaseLock($key = 'lock1')
   {
      global $redis;
      //返回可用資源到隊(duì)列
      $ret = $redis->rpush($key . '_list', 'lock_item1');
      return $ret;
   }

   /**
     * 釋放爭(zhēng)搶鎖
     * @param string $key 鎖的名稱
     * @return bool 釋放鎖成功/失敗
     */
   function releaseLock2($key = 'lock1')
   {
      global $redis;
      //刪除鎖
      $ret = $redis->del($key);
      return $ret;
   }

4. 測(cè)試

(1)正確性測(cè)試

使用ab測(cè)試工具喇肋,模擬并發(fā)請(qǐng)求


測(cè)試結(jié)果正確坟乾,一共成功執(zhí)行2000個(gè)請(qǐng)求,庫(kù)存只減到10蝶防。


測(cè)試結(jié)果正確甚侣,一共成功執(zhí)行1000個(gè)請(qǐng)求,庫(kù)存扣減到1000间学。

(2)和redis爭(zhēng)搶鎖對(duì)比測(cè)試

提示:示例代碼中的getLock2殷费、releaseLock2即為爭(zhēng)搶鎖例子

  • 效率對(duì)比
    兩種加鎖方式,分別ab測(cè)試2000個(gè)請(qǐng)求 100個(gè)并發(fā), php-fpm開(kāi)啟50個(gè)進(jìn)程
    ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)
  • 爭(zhēng)搶鎖執(zhí)行結(jié)果
    將初始庫(kù)存改為3000
    ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)

四. 最后

本文是提供了一個(gè)新的思路低葫,不完善的地方歡迎在評(píng)論區(qū)討論详羡。

五. 廣告

云服務(wù)器練手推薦

3月份騰訊云在打折促銷,新用戶1核2G云服務(wù)器99/年氮采,非新用戶可以注冊(cè)新賬號(hào)或者續(xù)費(fèi)也有優(yōu)惠殷绍。沒(méi)有云服務(wù)器的同學(xué)可以趁著打折去來(lái)一臺(tái)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鹊漠,隨后出現(xiàn)的幾起案子主到,更是在濱河造成了極大的恐慌,老刑警劉巖躯概,帶你破解...
    沈念sama閱讀 212,185評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件登钥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娶靡,警方通過(guò)查閱死者的電腦和手機(jī)牧牢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)姿锭,“玉大人塔鳍,你說(shuō)我怎么就攤上這事∩氪耍” “怎么了轮纫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,684評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)焚鲜。 經(jīng)常有香客問(wèn)我掌唾,道長(zhǎng)放前,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,564評(píng)論 1 284
  • 正文 為了忘掉前任糯彬,我火速辦了婚禮凭语,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撩扒。我一直安慰自己似扔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,681評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布却舀。 她就那樣靜靜地躺著虫几,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挽拔。 梳的紋絲不亂的頭發(fā)上辆脸,一...
    開(kāi)封第一講書(shū)人閱讀 49,874評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音螃诅,去河邊找鬼啡氢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛术裸,可吹牛的內(nèi)容都是我干的倘是。 我是一名探鬼主播,決...
    沈念sama閱讀 39,025評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼袭艺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搀崭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起猾编,我...
    開(kāi)封第一講書(shū)人閱讀 37,761評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘤睹,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后答倡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體轰传,經(jīng)...
    沈念sama閱讀 44,217評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,545評(píng)論 2 327
  • 正文 我和宋清朗相戀三年瘪撇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了获茬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,694評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倔既,死狀恐怖恕曲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渤涌,我是刑警寧澤码俩,帶...
    沈念sama閱讀 34,351評(píng)論 4 332
  • 正文 年R本政府宣布,位于F島的核電站歼捏,受9級(jí)特大地震影響稿存,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞳秽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,988評(píng)論 3 315
  • 文/蒙蒙 一瓣履、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧练俐,春花似錦袖迎、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,778評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至悯蝉,卻和暖如春归形,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鼻由。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,007評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工暇榴, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蕉世。 一個(gè)月前我還...
    沈念sama閱讀 46,427評(píng)論 2 360
  • 正文 我出身青樓蔼紧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親狠轻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奸例,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,580評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容