php如何應對秒殺搶購高並發思路

來源:互聯網
上載者:User

標籤:fifo   單線程   開始   發送   get   判斷   這一   memcache   mit   

我們常用QPS(Query Per Second,每秒處理請求數)來衡量一個web應用的吞吐率,解決每秒數萬次的高並發情境,這個指標非常關鍵。

舉個栗子:假設一個業務請求平均為100ms,同時系統內有20台apache web伺服器,MaxClients(apache的最大串連數)設定為500,那麼理論QPS峰值就是20*500/0.1=100000(理論與實際肯定有差異)。

這系統貌似理論上來說很強大1秒鐘處理100000個請求,實際當然沒有這麼理想。在高並發的實際情境下,機器都處於高負載的狀態,在這個時候平均回應時間會被大大增加。

就Web伺服器而言,Apache開啟了越多的串連進程,CPU需要處理的環境切換也越多,額外增加了CPU的消耗,然後就直接導致平均回應時間增加。因此上述的MaxClient數目,要根據CPU、記憶體等硬體因素綜合考慮,絕對不是越多越好。可以通過Apache內建的abench來測試一下,取一個合適的值。然後,我們選擇記憶體操作層級的儲存的Redis,在高並發的狀態下,儲存的回應時間至關重要。網路頻寬雖然也是一個因素,不過,這種請求資料包一般比較小,一般很少成為請求的瓶頸。負載平衡成為系統瓶頸的情況比較少,在這裡不做討論。

那麼問題來了,假設我們的系統,在5w/s的高並髮狀態下,平均回應時間從100ms變為250ms(實際情況,甚至更多):

20*500/0.25 = 40000 (4萬QPS)

於是,我們的系統剩下了4w的QPS,面對5w每秒的請求,中間相差了1w。

舉個例子,高速路口,1秒鐘來5部車,每秒通過5部車,高速路口運作正常。突然,這個路口1秒鐘只能通過4部車,車流量仍然依舊,結果必定出現大塞車。(5條車道忽然變成4條車道的感覺)

同理,某一個秒內,20*500個可用串連進程都在滿負荷工作中,卻仍然有1萬個新來請求,沒有串連進程可用,系統陷入到異常狀態也是預期之內。

 

其實在正常的非高並發的業務情境中,也有類似的情況出現,某個業務請求介面出現問題,回應時間極慢,將整個Web請求回應時間拉得很長,逐漸將Web伺服器的可用串連數佔滿,其他正常的業務請求,無串連進程可用。

更可怕的問題是,是使用者的行為特點,系統越是不可用,使用者的點擊越頻繁,惡性迴圈最終導致“雪崩”(其中一台Web機器掛了,導致流量分散到其他正常工作的機器上,再導致正常的機器也掛,然後惡性迴圈),將整個Web系統拖垮。

3. 重啟與過載保護

如果系統發生“雪崩”,貿然重啟服務,是無法解決問題的。最常見的現象是,啟動起來後,立刻掛掉。這個時候,最好在入口層將流量拒絕,然後再將重啟。如果是redis/memcache這種服務也掛了,重啟的時候需要注意“預熱”,並且很可能需要比較長的時間。

秒殺和搶購的情境,流量往往是超乎我們系統的準備和想象的。這個時候,過載保護是必要的。如果檢測到系統滿負載狀態,拒絕請求也是一種保護措施。在前端設定過濾是最簡單的方式,但是,這種做法是被使用者“千夫所指”的行為。更合適一點的是,將過載保護設定在CGI入口層,快速將客戶的直接請求返回

高並發下的資料安全

我們知道在多線程寫入同一個檔案的時候,會存現“安全執行緒”的問題(多個線程同時運行同一段代碼,如果每次運行結果和單線程啟動並執行結果是一樣的,結果和預期相同,就是安全執行緒的)。如果是MySQL資料庫,可以使用它內建的鎖機制很好的解決問題,但是,在大規模並發的情境中,是不推薦使用MySQL的。秒殺和搶購的情境中,還有另外一個問題,就是“超發”,如果在這方面控制不慎,會產生髮送過多的情況。我們也曾經聽說過,某些電商搞搶購活動,買家成功拍下後,商家卻不承認訂單有效,拒絕發貨。這裡的問題,也許並不一定是商家奸詐,而是系統技術層面存在超發風險導致的。

1. 超發的原因

假設某個搶購情境中,我們一共只有100個商品,在最後一刻,我們已經消耗了99個商品,僅剩最後一個。這個時候,系統發來多個並發請求,這批請求讀取到的商品餘量都是99個,然後都通過了這一個餘量判斷,最終導致超發。(導致了並發使用者B也“搶購成功”,多讓一個人獲得了商品。這種情境,在高並發的情況下非常容易出現。)

 

最佳化方案1:將庫存欄位number欄位設為unsigned,當庫存為0時,因為欄位不能為負數,將會返回false

<?php//最佳化方案1:將庫存欄位number欄位設為unsigned,當庫存為0時,因為欄位不能為負數,將會返回falseinclude(‘./mysql.php‘);$username = ‘wang‘.rand(0,1000);//產生唯一訂單function build_order_no(){  return date(‘ymd‘).substr(implode(NULL, array_map(‘ord‘, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//記錄日誌function insertLog($event,$type=0,$username){    global $conn;    $sql="insert into ih_log(event,type,usernma)    values(‘$event‘,‘$type‘,‘$username‘)";    return mysqli_query($conn,$sql);}function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number){      global $conn;      $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)      values(‘$order_sn‘,‘$user_id‘,‘$goods_id‘,‘$sku_id‘,‘$price‘,‘$username‘,‘$number‘)";     return  mysqli_query($conn,$sql);}//類比下單操作//庫存是否大於0$sql="select number from ih_store where goods_id=‘$goods_id‘ and sku_id=‘$sku_id‘ ";$rs=mysqli_query($conn,$sql);$row = $rs->fetch_assoc();  if($row[‘number‘]>0){//高並發下會導致超賣      if($row[‘number‘]<$number){        return insertLog(‘庫存不夠‘,3,$username);      }      $order_sn=build_order_no();      //庫存減少      $sql="update ih_store set number=number-{$number} where sku_id=‘$sku_id‘ and number>0";      $store_rs=mysqli_query($conn,$sql);      if($store_rs){          //產生訂單          insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);          insertLog(‘庫存減少成功‘,1,$username);      }else{          insertLog(‘庫存減少失敗‘,2,$username);      }  }else{      insertLog(‘庫存不夠‘,3,$username);  }?>

  

解決安全執行緒的思路很多,可以從“悲觀鎖”的方向開始討論。

悲觀鎖,也就是在修改資料的時候,採用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。

雖然上述的方案的確解決了安全執行緒的問題,但是,別忘記,我們的情境是“高並發”。也就是說,會很多這樣的修改請求,每個請求都需要等待“鎖”,某些線程可能永遠都沒有機會搶到這個“鎖”,這種請求就會死在那裡。同時,這種請求會很多,瞬間增大系統的平均回應時間,結果是可用串連數被耗盡,系統陷入異常。

最佳化方案2:使用MySQL的事務,鎖住操作的行

<?php//最佳化方案2:使用MySQL的事務,鎖住操作的行include(‘./mysql.php‘);//產生唯一訂單號function build_order_no(){  return date(‘ymd‘).substr(implode(NULL, array_map(‘ord‘, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//記錄日誌function insertLog($event,$type=0){    global $conn;    $sql="insert into ih_log(event,type)    values(‘$event‘,‘$type‘)";    mysqli_query($conn,$sql);}//類比下單操作//庫存是否大於0mysqli_query($conn,"BEGIN");  //開始事務$sql="select number from ih_store where goods_id=‘$goods_id‘ and sku_id=‘$sku_id‘ FOR UPDATE";//此時這條記錄被鎖住,其它事務必須等待此次事務提交後才能執行$rs=mysqli_query($conn,$sql);$row=$rs->fetch_assoc();if($row[‘number‘]>0){    //產生訂單    $order_sn=build_order_no();    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)    values(‘$order_sn‘,‘$user_id‘,‘$goods_id‘,‘$sku_id‘,‘$price‘)";    $order_rs=mysqli_query($conn,$sql);    //庫存減少    $sql="update ih_store set number=number-{$number} where sku_id=‘$sku_id‘";    $store_rs=mysqli_query($conn,$sql);    if($store_rs){      echo ‘庫存減少成功‘;        insertLog(‘庫存減少成功‘);        mysqli_query($conn,"COMMIT");//事務提交即解鎖    }else{      echo ‘庫存減少失敗‘;        insertLog(‘庫存減少失敗‘);    }}else{  echo ‘庫存不夠‘;    insertLog(‘庫存不夠‘);    mysqli_query($conn,"ROLLBACK");}?>

  

3. FIFO隊列思路

那好,那麼我們稍微修改一下上面的情境,我們直接將請求放入隊列中的,採用FIFO(First Input First Output,先進先出),這樣的話,我們就不會導致某些請求永遠擷取不到鎖。看到這裡,是不是有點強行將多線程變成單線程的感覺哈。

然後,我們現在解決了鎖的問題,全部請求採用“先進先出”的隊列方式來處理。那麼新的問題來了,高並發的情境下,因為請求很多,很可能一瞬間將隊列記憶體“撐爆”,然後系統又陷入到了異常狀態。或者設計一個極大的記憶體隊列,也是一種方案,但是,系統處理完一個隊列內請求的速度根本無法和瘋狂湧入隊列中的數目相比。也就是說,隊列內的請求會越積累越多,最終Web系統平均響應時候還是會大幅下降,系統還是陷入異常。

4. 檔案鎖的思路

對於日IP不高或者說並發數不是很大的應用,一般不用考慮這些!用一般的檔案操作方法完全沒有問題。但如果並發高,在我們對檔案進行讀寫操作時,很有可能多個進程對進一檔案進行操作,如果這時不對檔案的訪問進行相應的獨佔,就容易造成資料丟失

最佳化方案4:使用非阻塞的檔案獨佔鎖定

<?php//最佳化方案4:使用非阻塞的檔案獨佔鎖定include (‘./mysql.php‘);//產生唯一訂單號function build_order_no(){  return date(‘ymd‘).substr(implode(NULL, array_map(‘ord‘, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//記錄日誌function insertLog($event,$type=0){    global $conn;    $sql="insert into ih_log(event,type)    values(‘$event‘,‘$type‘)";    mysqli_query($conn,$sql);}$fp = fopen("lock.txt", "w+");if(!flock($fp,LOCK_EX | LOCK_NB)){    echo "系統繁忙,請稍後再試";    return;}//下單$sql="select number from ih_store where goods_id=‘$goods_id‘ and sku_id=‘$sku_id‘";$rs =  mysqli_query($conn,$sql);$row = $rs->fetch_assoc();if($row[‘number‘]>0){//庫存是否大於0    //類比下單操作    $order_sn=build_order_no();    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)    values(‘$order_sn‘,‘$user_id‘,‘$goods_id‘,‘$sku_id‘,‘$price‘)";    $order_rs =  mysqli_query($conn,$sql);    //庫存減少    $sql="update ih_store set number=number-{$number} where sku_id=‘$sku_id‘";    $store_rs =  mysqli_query($conn,$sql);    if($store_rs){      echo ‘庫存減少成功‘;        insertLog(‘庫存減少成功‘);        flock($fp,LOCK_UN);//釋放鎖    }else{      echo ‘庫存減少失敗‘;        insertLog(‘庫存減少失敗‘);    }}else{  echo ‘庫存不夠‘;    insertLog(‘庫存不夠‘);}fclose($fp); ?>

  

5. 樂觀鎖思路

這個時候,我們就可以討論一下“樂觀鎖”的思路了。樂觀鎖,是相對於“悲觀鎖”採用更為寬鬆的加鎖機制,大都是採用帶版本號碼(Version)更新。實現就是,這個資料所有請求都有資格去修改,但會獲得一個該資料的版本號碼,只有版本號碼符合的才能更新成功,其他的返回搶購失敗。這樣的話,我們就不需要考慮隊列的問題,不過,它會增大CPU的計算開銷。但是,綜合來說,這是一個比較好的解決方案。

有很多軟體和服務都“樂觀鎖”功能的支援,例如Redis中的watch就是其中之一。通過這個實現,我們保證了資料的安全。

最佳化方案5:Redis中的watch

<?php$redis = new redis(); $result = $redis->connect(‘127.0.0.1‘, 6379); echo $mywatchkey = $redis->get("mywatchkey");/*  //插入搶購資料 if($mywatchkey>0) {     $redis->watch("mywatchkey");  //啟動一個新的事務。    $redis->multi();   $redis->set("mywatchkey",$mywatchkey-1);   $result = $redis->exec();   if($result) {      $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());      $watchkeylist = $redis->hGetAll("watchkeylist");        echo "搶購成功!<br/>";         $re = $mywatchkey - 1;           echo "剩餘數量:".$re."<br/>";        echo "使用者列表:<pre>";        print_r($watchkeylist);   }else{      echo "手氣不好,再搶購!";exit;   }   }else{     // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");     //  $watchkeylist = $redis->hGetAll("watchkeylist");        echo "fail!<br/>";            echo ".no result<br/>";        echo "使用者列表:<pre>";      //  var_dump($watchkeylist);   }*/$rob_total = 100;   //搶購數量if($mywatchkey<=$rob_total){    $redis->watch("mywatchkey");    $redis->multi(); //在當前串連上啟動一個新的事務。    //插入搶購資料    $redis->set("mywatchkey",$mywatchkey+1);    $rob_result = $redis->exec();    if($rob_result){         $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);        $mywatchlist = $redis->hGetAll("watchkeylist");        echo "搶購成功!<br/>";             echo "剩餘數量:".($rob_total-$mywatchkey-1)."<br/>";        echo "使用者列表:<pre>";        var_dump($mywatchlist);    }else{          $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),‘meiqiangdao‘);        echo "手氣不好,再搶購!";exit;    }}?>

  暫時只想到這些,如果有什麼新方法可以評論區討論交流

 

php如何應對秒殺搶購高並發思路

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.