主頁 > 知識庫 > PHP商品秒殺問題解決方案實例詳解【mysql與redis】

PHP商品秒殺問題解決方案實例詳解【mysql與redis】

熱門標簽:廣東地市地圖標注 外呼系統(tǒng)撥打暫時無法接通 長春人工外呼系統(tǒng)服務商 江西手機自動外呼防封系統(tǒng)是什么 廣州防封卡外呼系統(tǒng)多少錢一個月 仁和怎么申請400開頭的電話 高德地圖標注家 怎么向銷售公司推銷外呼系統(tǒng) 哪里辦理400電話

本文實例講述了PHP商品秒殺問題解決方案。分享給大家供大家參考,具體如下:

引言

假設num是存儲在數(shù)據(jù)庫中的字段,保存了被秒殺產品的剩余數(shù)量。

if($num > 0){
  //用戶搶購成功,記錄用戶信息
  $num--;
}

假設在一個并發(fā)量較高的場景,數(shù)據(jù)庫中num的值為1時,可能同時會有多個進程讀取到num為1,程序判斷符合條件,搶購成功,num減一。這樣會導致商品超發(fā)的情況,本來只有10件可以搶購的商品,可能會有超過10個人搶到,此時num在搶購完成之后為負值。

解決該問題的方案由很多,可以簡單分為基于mysql和redis的解決方案,redis的性能要由于mysql,因此可以承載更高的并發(fā)量,不過下面介紹的方案都是基于單臺mysql和redis的,更高的并發(fā)量需要分布式的解決方案,本文沒有涉及。

基于mysql的解決方案

商品表 goods

CREATE TABLE `goods` (
 `id` int(11) NOT NULL,
 `num` int(11) DEFAULT NULL,
 `version` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

搶購結果表 log

CREATE TABLE `log` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `good_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

悲觀鎖

悲觀鎖的方案采用的是排他讀,也就是同時只能有一個進程讀取到num的值。事務在提交或回滾之后,鎖會釋放,其他的進程才能讀取。該方案最簡單易懂,在對性能要求不高時,可以直接采用該方案。要注意的是,SELECT … FOR UPDATE要盡可能的使用索引,以便鎖定盡可能少的行數(shù);排他鎖是在事務執(zhí)行結束之后才釋放的,不是讀取完成之后就釋放,因此使用的事務應該盡可能的早些提交或回滾,以便早些釋放排它鎖。

$this->mysqli->begin_transaction();
$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1 FOR UPDATE");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->query("UPDATE goods SET num=num-1");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  $this->mysqli->commit();
  echo "fail3:".$num;
}

樂觀鎖

樂觀鎖的方案在讀取數(shù)據(jù)是并沒有加排他鎖,而是通過一個每次更新都會自增的version字段來解決,多個進程讀取到相同num,然后都能更新成功的問題。在每個進程讀取num的同時,也讀取version的值,并且在更新num的同時也更新version,并在更新時加上對version的等值判斷。假設有10個進程都讀取到了num的值為1,version值為9,則這10個進程執(zhí)行的更新語句都是UPDATE goods SET num=num-1,version=version+1 WHERE version=9,然而當其中一個進程執(zhí)行成功之后,數(shù)據(jù)庫中version的值就會變?yōu)?0,剩余的9個進程都不會執(zhí)行成功,這樣保證了商品不會超發(fā),num的值不會小于0,但這也導致了一個問題,那就是發(fā)出搶購請求較早的用戶可能搶不到,反而被后來的請求搶到了。

$result = $this->mysqli->query("SELECT num,version FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
$version = intval($row['version']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1,version=version+1 WHERE version={$version}");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}

where條件(原子操作)

悲觀鎖的方案保證了數(shù)據(jù)庫中num的值在同一時間只能被一個進程讀取并處理,也就是并發(fā)的讀取進程到這里要排隊依次執(zhí)行。樂觀鎖的方案雖然num的值可以被多個進程同時讀取到,但是更新操作中version的等值判斷可以保證并發(fā)的更新操作在同一時間只能有一個更新成功。

還有一種更簡單的方案,只在更新操作時加上num>0的條件限制即可。通過where條件限制的方案雖然看似和樂觀鎖方案類似,都能夠防止超發(fā)問題的出現(xiàn),但在num較大時的表現(xiàn)還是有很大區(qū)別的。假如此時num為10,同時有5個進程讀取到了num=10,對于樂觀鎖的方案由于version字段的等值判斷,這5個進程只會有一個更新成功,這5個進程執(zhí)行完成之后num為9;對于where條件判斷的方案,只要num>0都能夠更新成功,這5個進程執(zhí)行完成之后num為5。

$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1");
$row = $result->fetch_assoc();
$num = intval($row['num']);
if($num > 0){
  usleep(100);
  $this->mysqli->begin_transaction();
  $this->mysqli->query("UPDATE goods SET num=num-1 WHERE num>0");
  $affected_rows = $this->mysqli->affected_rows;
  if($affected_rows == 1){
    $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})");
    $affected_rows = $this->mysqli->affected_rows;
    if($affected_rows == 1){
      $this->mysqli->commit();
      echo "success:".$num;
    }else{
      $this->mysqli->rollback();
      echo "fail1:".$num;
    }
  }else{
    $this->mysqli->rollback();
    echo "fail2:".$num;
  }
}else{
  echo "fail3:".$num;
}

基于redis的解決方案

基于watch的樂觀鎖方案

watch用于監(jiān)視一個(或多個) key ,如果在事務執(zhí)行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷。這種方案跟mysql中的樂觀鎖方案類似,具體表現(xiàn)也是一樣的。

$num = $this->redis->get('num');
if($num > 0) {
  $this->redis->watch('num');
  usleep(100);
  $res = $this->redis->multi()->decr('num')->lPush('result',$num)->exec();
  if($res == false){
    echo "fail1";
  }else{
    echo "success:".$num;
  }
}else{
  echo "fail2";
}

基于list的隊列方案

基于隊列的方案利用了redis出隊操作的原子性,搶購開始之前首先將商品編號放入響應的隊列中,在搶購時依次從隊列中彈出操作,這樣可以保證每個商品只能被一個進程獲取并操作,不存在超發(fā)的情況。該方案的優(yōu)點是理解和實現(xiàn)起來都比較簡單,缺點是當商品數(shù)量較多是,需要將大量的數(shù)據(jù)存入到隊列中,并且不同的商品需要存入到不同的消息隊列中。

public function init(){
  $this->redis->del('goods');
  for($i=1;$i=10;$i++){
    $this->redis->lPush('goods',$i);
  }
  $this->redis->del('result');
  echo 'init done';
}
public function run(){
  $goods_id = $this->redis->rPop('goods');
  usleep(100);
  if($goods_id == false) {
    echo "fail1";
  }else{
    $res = $this->redis->lPush('result',$goods_id);
    if($res == false){
      echo "writelog:".$goods_id;
    }else{
      echo "success".$goods_id;
    }
  }
}

基于decr返回值的方案

如果我們將剩余量num設置為一個鍵值類型,每次先get之后判斷,然后再decr是不能解決超發(fā)問題的。但是redis中的decr操作會返回執(zhí)行后的結果,可以解決超發(fā)問題。我們首先get到num的值進行第一步判斷,避免每次都去更新num的值,然后再對num執(zhí)行decr操作,并判斷decr的返回值,如果返回值不小于0,這說明decr之前是大于0的,用戶搶購成功。

public function run(){
  $num = $this->redis->get('num');
  if($num > 0) {
    usleep(100);
    $retNum = $this->redis->decr('num');
    if($retNum >= 0){
      $res = $this->redis->lPush('result',$retNum);
      if($res == false){
        echo "writeLog:".$retNum;
      }else{
        echo "success:".$retNum;
      }
    }else{
      echo "fail1";
    }
  }else{
    echo "fail2";
  }
}

基于setnx的排它鎖方案

redis沒有像mysql中的排它鎖,但是可以通過一些方式實現(xiàn)排它鎖的功能,就類似php使用文件鎖實現(xiàn)排它鎖一樣。

setnx實現(xiàn)了exists和set兩個指令的功能,若給定的key已存在,則setnx不做任何動作,返回0;若key不存在,則執(zhí)行類似set的操作,返回1。我們設置一個超時時間timeout,每隔一定時間嘗試setnx操作,如果設置成功就是獲得了相應的鎖,執(zhí)行num的decr操作,操作完成刪除相應的key,模擬釋放鎖的操作。

public function run(){
  do {
    $res = $this->redis->setnx("numKey",1);
    $this->timeout -= 100;
    usleep(100);
  }while($res == 0  $this->timeout>0);
  if($res == 0){
    echo 'fail1';
  }else{
    $num = $this->redis->get('num');
    if($num > 0) {
      $this->redis->decr('num');
      usleep(100);
      $res = $this->redis->lPush('result',$num);
      if($res == false){
        echo "fail2";
      }else{
        echo "success:".$num;
      }
    }else{
      echo "fail3";
    }
    $this->redis->del("numKey");
  }
}

上述代碼都在本地測試通過,完整代碼地址:https://github.com/qianshou/SeckillSolution

更多關于PHP相關內容感興趣的讀者可查看本站專題:《php+mysql數(shù)據(jù)庫操作入門教程》、《php+redis數(shù)據(jù)庫程序設計技巧總結》、《php面向對象程序設計入門教程》、《PHP基本語法入門教程》、《PHP數(shù)組(Array)操作技巧大全》、《php字符串(string)用法總結》及《php常見數(shù)據(jù)庫操作技巧匯總》

希望本文所述對大家PHP程序設計有所幫助。

您可能感興趣的文章:
  • PHP Redis擴展無法加載的問題解決方法
  • thinkPHP框架通過Redis實現(xiàn)增刪改查操作的方法詳解
  • PHP+redis實現(xiàn)的限制搶購防止商品超發(fā)功能詳解
  • PHP+Redis鏈表解決高并發(fā)下商品超賣問題(實現(xiàn)原理及步驟)

標簽:海北 黔東 廈門 梅河口 文山 濮陽 惠州 湘西

巨人網(wǎng)絡通訊聲明:本文標題《PHP商品秒殺問題解決方案實例詳解【mysql與redis】》,本文關鍵詞  PHP,商品,秒殺,問題,解決方案,;如發(fā)現(xiàn)本文內容存在版權問題,煩請?zhí)峁┫嚓P信息告之我們,我們將及時溝通與處理。本站內容系統(tǒng)采集于網(wǎng)絡,涉及言論、版權與本站無關。
  • 相關文章
  • 下面列出與本文章《PHP商品秒殺問題解決方案實例詳解【mysql與redis】》相關的同類信息!
  • 本頁收集關于PHP商品秒殺問題解決方案實例詳解【mysql與redis】的相關信息資訊供網(wǎng)民參考!
  • 推薦文章