1. 程式人生 > 其它 >php秒殺搶購實現

php秒殺搶購實現

搶購、秒殺是平常很常見的場景,面試的時候面試官也經常會問到,比如問你淘寶中的搶購秒殺是怎麼實現的等等。 搶購、秒殺實現很簡單,但是有些問題需要解決,主要針對兩個問題: 一、高併發對資料庫產生的壓力 二、競爭狀態下如何解決庫存的正確減少("超賣"問題) 第一個問題,對於PHP來說很簡單,用快取技術就可以緩解資料庫壓力,比如memcache,redis等快取技術。 第二個問題就比較複雜點: 常規寫法: 查詢出對應商品的庫存,看是否大於0,然後執行生成訂單等操作,但是在判斷庫存是否大於0處,如果在高併發下就會有問題,導致庫存量出現負數。
  1. $conn=mysql_connect("localhost","big","123456");
  2. if(!$conn){
  3. echo"connectfailed";
  4. exit;
  5. }
  6. mysql_select_db("big",$conn);
  7. mysql_query("setnamesutf8");
  8. $price=10;
  9. $user_id=1;
  10. $goods_id=1;
  11. $sku_id=11;
  12. $number=1;
  13. //生成唯一訂單
  14. functionbuild_order_no(){
  15. returndate('ymd').substr(implode(NULL,array_map('ord',str_split(substr(uniqid(),7,13),1))),0,8);
  16. }
  17. //記錄日誌
  18. functioninsertLog($event,$type=0){
  19. global$conn;
  20. $sql="insertintoih_log(event,type)
  21. values('$event','$type')";
  22. mysql_query($sql,$conn);
  23. }
  24. //模擬下單操作
  25. //庫存是否大於0
  26. $sql="selectnumberfromih_storewheregoods_id='$goods_id'andsku_id='$sku_id'";
  27. //解鎖此時ih_store資料中goods_id='$goods_id'andsku_id='$sku_id'的資料被鎖住(注3),其它事務必須等待此次事務提交後才能執行
  28. $rs=mysql_query($sql,$conn);
  29. $row=mysql_fetch_assoc($rs);
  30. if($row['number']>0){//高併發下會導致超賣
  31. $order_sn=build_order_no();
  32. //生成訂單
  33. $sql="insertintoih_order(order_sn,user_id,goods_id,sku_id,price)
  34. values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
  35. $order_rs=mysql_query($sql,$conn);
  36. //庫存減少
  37. $sql="updateih_storesetnumber=number-{$number}wheresku_id='$sku_id'";
  38. $store_rs=mysql_query($sql,$conn);
  39. if(mysql_affected_rows()){
  40. insertLog('庫存減少成功');
  41. }else{
  42. insertLog('庫存減少失敗');
  43. }
  44. }else{
  45. insertLog('庫存不夠');
  46. }
複製程式碼 出現這種情況怎麼辦呢?來看幾種優化方法: 優化方案1:將庫存欄位number欄位設為unsigned,當庫存為0時,因為欄位不能為負數,將會返回false
  1. 1//庫存減少
  2. 2$sql="updateih_storesetnumber=number-{$number}wheresku_id='$sku_id'andnumber>0";
  3. 3$store_rs=mysql_query($sql,$conn);
  4. 4if(mysql_affected_rows()){
  5. 5insertLog('庫存減少成功');6}
複製程式碼 優化方案2:使用MySQL的事務,鎖住操作的行
  1. $conn=mysql_connect("localhost","big","123456");
  2. if(!$conn){
  3. echo"connectfailed";
  4. exit;
  5. }
  6. mysql_select_db("big",$conn);
  7. mysql_query("setnamesutf8");
  8. $price=10;
  9. $user_id=1;
  10. $goods_id=1;
  11. $sku_id=11;
  12. $number=1;
  13. //生成唯一訂單號
  14. functionbuild_order_no(){
  15. returndate('ymd').substr(implode(NULL,array_map('ord',str_split(substr(uniqid(),7,13),1))),0,8);
  16. }
  17. //記錄日誌
  18. functioninsertLog($event,$type=0){
  19. global$conn;
  20. $sql="insertintoih_log(event,type)
  21. values('$event','$type')";
  22. mysql_query($sql,$conn);
  23. }
  24. //模擬下單操作
  25. //庫存是否大於0
  26. mysql_query("BEGIN");//開始事務
  27. $sql="selectnumberfromih_storewheregoods_id='$goods_id'andsku_id='$sku_id'FORUPDATE";//此時這條記錄被鎖住,其它事務必須等待此次事務提交後才能執行
  28. $rs=mysql_query($sql,$conn);
  29. $row=mysql_fetch_assoc($rs);
  30. if($row['number']>0){
  31. //生成訂單
  32. $order_sn=build_order_no();
  33. $sql="insertintoih_order(order_sn,user_id,goods_id,sku_id,price)
  34. values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
  35. $order_rs=mysql_query($sql,$conn);
  36. //庫存減少
  37. $sql="updateih_storesetnumber=number-{$number}wheresku_id='$sku_id'";
  38. $store_rs=mysql_query($sql,$conn);
  39. if(mysql_affected_rows()){
  40. insertLog('庫存減少成功');
  41. mysql_query("COMMIT");//事務提交即解鎖
  42. }else{
  43. insertLog('庫存減少失敗');
  44. }
  45. }else{
  46. insertLog('庫存不夠');
  47. mysql_query("ROLLBACK");
  48. }
複製程式碼 優化方案3:使用非阻塞的檔案排他鎖
  1. $conn=mysql_connect("localhost","root","123456");
  2. if(!$conn){
  3. echo"connectfailed";
  4. exit;
  5. }
  6. mysql_select_db("big-bak",$conn);
  7. mysql_query("setnamesutf8");
  8. $price=10;
  9. $user_id=1;
  10. $goods_id=1;
  11. $sku_id=11;
  12. $number=1;
  13. //生成唯一訂單號
  14. functionbuild_order_no(){
  15. returndate('ymd').substr(implode(NULL,array_map('ord',str_split(substr(uniqid(),7,13),1))),0,8);
  16. }
  17. //記錄日誌
  18. functioninsertLog($event,$type=0){
  19. global$conn;
  20. $sql="insertintoih_log(event,type)
  21. values('$event','$type')";
  22. mysql_query($sql,$conn);
  23. }
  24. $fp=fopen("lock.txt","w+");
  25. if(!flock($fp,LOCK_EX|LOCK_NB)){
  26. echo"系統繁忙,請稍後再試";
  27. return;
  28. }
  29. //下單
  30. $sql="selectnumberfromih_storewheregoods_id='$goods_id'andsku_id='$sku_id'";
  31. $rs=mysql_query($sql,$conn);
  32. $row=mysql_fetch_assoc($rs);
  33. if($row['number']>0){//庫存是否大於0
  34. //模擬下單操作
  35. $order_sn=build_order_no();
  36. $sql="insertintoih_order(order_sn,user_id,goods_id,sku_id,price)
  37. values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
  38. $order_rs=mysql_query($sql,$conn);
  39. //庫存減少
  40. $sql="updateih_storesetnumber=number-{$number}wheresku_id='$sku_id'";
  41. $store_rs=mysql_query($sql,$conn);
  42. if(mysql_affected_rows()){
  43. insertLog('庫存減少成功');
  44. flock($fp,LOCK_UN);//釋放鎖
  45. }else{
  46. insertLog('庫存減少失敗');
  47. }
  48. }else{
  49. insertLog('庫存不夠');
  50. }
  51. fclose($fp);
複製程式碼 優化方案4:使用redis佇列,因為pop操作是原子的,即使有很多使用者同時到達,也是依次執行,推薦使用(mysql事務在高併發下效能下降很厲害,檔案鎖的方式也是) 先將商品庫存如佇列
  1. $store=1000;
  2. $redis=newRedis();
  3. $result=$redis->connect('127.0.0.1',6379);
  4. $res=$redis->llen('goods_store');
  5. echo$res;
  6. $count=$store-$res;
  7. for($i=0;$i<$count;$i++){
  8. $redis->lpush('goods_store',1);
  9. }
  10. echo$redis->llen('goods_store');
複製程式碼 搶購、描述邏輯
  1. $conn=mysql_connect("localhost","big","123456");
  2. if(!$conn){
  3. echo"connectfailed";
  4. exit;
  5. }
  6. mysql_select_db("big",$conn);
  7. mysql_query("setnamesutf8");
  8. $price=10;
  9. $user_id=1;
  10. $goods_id=1;
  11. $sku_id=11;
  12. $number=1;
  13. //生成唯一訂單號
  14. functionbuild_order_no(){
  15. returndate('ymd').substr(implode(NULL,array_map('ord',str_split(substr(uniqid(),7,13),1))),0,8);
  16. }
  17. //記錄日誌
  18. functioninsertLog($event,$type=0){
  19. global$conn;
  20. $sql="insertintoih_log(event,type)
  21. values('$event','$type')";
  22. mysql_query($sql,$conn);
  23. }
  24. //模擬下單操作
  25. //下單前判斷redis佇列庫存量
  26. $redis=newRedis();
  27. $result=$redis->connect('127.0.0.1',6379);
  28. $count=$redis->lpop('goods_store');
  29. if(!$count){
  30. insertLog('error:nostoreredis');
  31. return;
  32. }
  33. //生成訂單
  34. $order_sn=build_order_no();
  35. $sql="insertintoih_order(order_sn,user_id,goods_id,sku_id,price)
  36. values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
  37. $order_rs=mysql_query($sql,$conn);
  38. //庫存減少
  39. $sql="updateih_storesetnumber=number-{$number}wheresku_id='$sku_id'";
  40. $store_rs=mysql_query($sql,$conn);
  41. if(mysql_affected_rows()){
  42. insertLog('庫存減少成功');
  43. }else{
  44. insertLog('庫存減少失敗');
  45. }
複製程式碼 上述只是簡單模擬高併發下的搶購,真實場景要比這複雜很多,很多注意的地方,如搶購頁面做成靜態的,通過ajax呼叫介面。 再如上面的會導致一個使用者搶多個,思路: 需要一個排隊佇列和搶購結果佇列及庫存佇列。高併發情況,先將使用者進入排隊佇列,用一個執行緒迴圈處理從排隊佇列取出一個使用者,判斷使用者是否已在搶購結果佇列,如果在,則已搶購,否則未搶購,庫存減1,寫資料庫,將使用者入結果佇列。 我之間做商城專案的時候,在秒殺這一塊我直接用的redis,這段時間看了看上面的幾種方法,雖然各有不同,但是實現目的都一樣的,各位自己選擇,開心就好。