php 高並發下 秒殺處理思路


1.用額外的單進程處理一個隊列,下單請求放到隊列里,一個個處理,就不會有並發的問題了,但是要額外的后台進程以及延遲問題,不予考慮。

2.數據庫樂觀鎖,大致的意思是先查詢庫存,然后立馬將庫存+1,然后訂單生成后,在更新庫存前再查詢一次庫存,看看跟預期的庫存數量是否保持一致,不一致就回滾,提示用戶庫存不足。

3.根據update結果來判斷,我們可以在sql2的時候加一個判斷條件update ... where 庫存>0,如果返回false,則說明庫存不足,並回滾事務。

4.借助文件排他鎖,在處理下單請求的時候,用flock鎖定一個文件,如果鎖定失敗說明有其他訂單正在處理,此時要么等待要么直接提示用戶"服務器繁忙"

 

搶購、秒殺是平常很常見的場景,面試的時候面試官也經常會問到,比如問你淘寶中的搶購秒殺是怎么實現的等等。

搶購、秒殺實現很簡單,但是有些問題需要解決,主要針對兩個問題:

一、高並發對數據庫產生的壓力
二、競爭狀態下如何解決庫存的正確減少("超賣"問題)

第一個問題,對於PHP來說很簡單,用緩存技術就可以緩解數據庫壓力,比如memcache,redis等緩存技術。
第二個問題就比較復雜點:

常規寫法:
查詢出對應商品的庫存,看是否大於0,然后執行生成訂單等操作,但是在判斷庫存是否大於0處,如果在高並發下就會有問題,導致庫存量出現負數。

<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){
echo "connect failed";
exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");

$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;

//生成唯一訂單
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')";
mysql_query($sql,$conn);
}

//模擬下單操作
//庫存是否大於0
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
//解鎖 此時ih_store數據中goods_id='$goods_id' and sku_id='$sku_id' 的數據被鎖住(注3),其它事務必須等待此次事務 提交后才能執行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
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=mysql_query($sql,$conn);

//庫存減少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog('庫存減少成功');
}else{
insertLog('庫存減少失敗');
}
}else{
insertLog('庫存不夠');
}

出現這種情況怎么辦呢?來看幾種優化方法:

優化方案1:將庫存字段number字段設為unsigned,當庫存為0時,因為字段不能為負數,將會返回false

//庫存減少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog('庫存減少成功');
}

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

<?php
$conn=mysql_connect("localhost","big","123456");
if(!$conn){
echo "connect failed";
exit;
}
mysql_select_db("big",$conn);
mysql_query("set names utf8");

$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;

//生成唯一訂單號
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')";
mysql_query($sql,$conn);
}

//模擬下單操作
//庫存是否大於0
mysql_query("BEGIN"); //開始事務
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時這條記錄被鎖住,其它事務必須等待此次事務提交后才能執行
$rs=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
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=mysql_query($sql,$conn);

//庫存減少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog('庫存減少成功');
mysql_query("COMMIT");//事務提交即解鎖
}else{
insertLog('庫存減少失敗');
}
}else{
insertLog('庫存不夠');
mysql_query("ROLLBACK");
}

復制代碼

優化方案3:使用非阻塞的文件排他鎖

<?php
$conn=mysql_connect("localhost","root","123456");
if(!$conn){
echo "connect failed";
exit;
}
mysql_select_db("big-bak",$conn);
mysql_query("set names utf8");

$price=10;
$user_id=1;
$goods_id=1;
$sku_id=11;
$number=1;

//生成唯一訂單號
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')";
mysql_query($sql,$conn);
}

$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=mysql_query($sql,$conn);
$row=mysql_fetch_assoc($rs);
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=mysql_query($sql,$conn);

//庫存減少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysql_query($sql,$conn);
if(mysql_affected_rows()){
insertLog('庫存減少成功');
flock($fp,LOCK_UN);//釋放鎖
}else{
insertLog('庫存減少失敗');
}
}else{
insertLog('庫存不夠');
}
fclose($fp);

復制代碼

優化方案4:使用redis隊列,因為pop操作是原子的,即使有很多用戶同時到達,也是依次執行,推薦使用(mysql事務在高並發下性能下降很厲害,文件鎖的方式也是)
先將商品庫存如隊列

<?php
$store=1000;
$redis=new Redis();
$result=$redis->connect('127.0.0.1',6379);
$res=$redis->llen('goods_store');
echo $res;
$count=$store-$res;
for($i=0;$i<$count;$i++){
$redis->lpush('goods_store',1);
}
echo $redis->llen('goods_store');

搶購、描述邏輯
  1. <?php
  2. $conn=mysql_connect("localhost","big","123456");
  3. if(!$conn){
  4.     echo "connect failed";
  5.     exit;
  6. }
  7. mysql_select_db("big",$conn);
  8. mysql_query("set names utf8");
  9.   
  10. $price=10;
  11. $user_id=1;
  12. $goods_id=1;
  13. $sku_id=11;
  14. $number=1;
  15.   
  16. //生成唯一訂單號
  17. function build_order_no(){
  18.   return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
  19. }
  20. //記錄日志
  21. function insertLog($event,$type=0){
  22.     global $conn;
  23.     $sql="insert into ih_log(event,type)
  24.     values('$event','$type')";
  25.     mysql_query($sql,$conn);
  26. }
  27.   
  28. //模擬下單操作
  29. //下單前判斷redis隊列庫存量
  30. $redis=new Redis();
  31. $result=$redis->connect('127.0.0.1',6379);
  32. $count=$redis->lpop('goods_store');
  33. if(!$count){
  34.     insertLog('error:no store redis');
  35.     return;
  36. }
  37.   
  38. //生成訂單
  39. $order_sn=build_order_no();
  40. $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
  41. values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
  42. $order_rs=mysql_query($sql,$conn);
  43.   
  44. //庫存減少
  45. $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
  46. $store_rs=mysql_query($sql,$conn);
  47. if(mysql_affected_rows()){
  48.     insertLog('庫存減少成功');
  49. }else{
  50.     insertLog('庫存減少失敗');
  51. }
復制代碼

上述只是簡單模擬高並發下的搶購,真實場景要比這復雜很多,很多注意的地方,如搶購頁面做成靜態的,通過ajax調用接口。

再如上面的會導致一個用戶搶多個,思路:
需要一個排隊隊列和搶購結果隊列及庫存隊列。高並發情況,先將用戶進入排隊隊列,用一個線程循環處理從排隊隊列取出一個用戶,判斷用戶是否已在搶購結果隊列,如果在,則已搶購,否則未搶購,庫存減1,寫數據庫,將用戶入結果隊列。
我之間做商城項目的時候,在秒殺這一塊我直接用的redis,這段時間看了看上面的幾種方法,雖然各有不同,但是實現目的都一樣的,各位自己選擇,開心就好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM