1. 程式人生 > >PHP結合redis實現的高併發下----搶購、秒殺功能

PHP結合redis實現的高併發下----搶購、秒殺功能

搶購、秒殺是如今很常見的一個應用場景,主要需要解決的問題有兩個:
1 高併發對資料庫產生的壓力
2 競爭狀態下如何解決庫存的正確減少("超賣"問題)
對於第一個問題,已經很容易想到用鎖表來處理搶購,但是鎖表漏洞是  假設一個使用者出現問題程式就不能接著運行了 ,例如使用Redis。

重點在於第二個問題

模擬高並發現象可參考   http://blog.csdn.net/nuli888/article/details/51865401  

  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. $price=10;  
  10. $user_id=1;  
  11. $goods_id=1;  
  12. $sku_id=11;  
  13. $number=1;  
  14. //生成唯一訂單
  15. function build_order_no(){  
  16.     returndate('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);  
  17. }  
  18. //記錄日誌
  19. function insertLog($event,$type=0){  
  20.     global$conn;  
  21.     $sql="insert into ih_log(event,type)   
  22.     values('$event','$type')";    
  23.     mysql_query($sql,$conn);    
  24. }  
  25. //模擬下單操作
  26. //庫存是否大於0
  27. $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),其它事務必須等待此次事務 提交後才能執行
  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="insert into ih_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="update ih_store set number=number-{$number} where sku_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. }  
  47. ?> 

測試資料表

  1. --  
  2. -- 資料庫: `big`  
  3. --  
  4. -- --------------------------------------------------------  
  5. --  
  6. -- 表的結構 `ih_goods`  
  7. --  
  8. CREATE TABLE IF NOT EXISTS `ih_goods` (  
  9.   `goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,  
  10.   `cat_id` int(11) NOT NULL,  
  11.   `goods_name` varchar(255) NOT NULL,  
  12.   PRIMARY KEY (`goods_id`)  
  13. ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;  
  14. --  
  15. -- 轉存表中的資料 `ih_goods`  
  16. --  
  17. INSERT INTO `ih_goods` (`goods_id`, `cat_id`, `goods_name`) VALUES  
  18. (1, 0, '小米手機');  
  19. -- --------------------------------------------------------  
  20. --  
  21. -- 表的結構 `ih_log`  
  22. --  
  23. CREATE TABLE IF NOT EXISTS `ih_log` (  
  24.   `id` int(11) NOT NULL AUTO_INCREMENT,  
  25.   `event` varchar(255) NOT NULL,  
  26.   `type` tinyint(4) NOT NULL DEFAULT '0',  
  27.   `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,  
  28.   PRIMARY KEY (`id`)  
  29. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;  
  30. --  
  31. -- 轉存表中的資料 `ih_log`  
  32. --  
  33. -- --------------------------------------------------------  
  34. --  
  35. -- 表的結構 `ih_order`  
  36. --  
  37. CREATE TABLE IF NOT EXISTS `ih_order` (  
  38.   `id` int(11) NOT NULL AUTO_INCREMENT,  
  39.   `order_sn` char(32) NOT NULL,  
  40.   `user_id` int(11) NOT NULL,  
  41.   `status` int(11) NOT NULL DEFAULT '0',  
  42.   `goods_id` int(11) NOT NULL DEFAULT '0',  
  43.   `sku_id` int(11) NOT NULL DEFAULT '0',  
  44.   `price` float NOT NULL,  
  45.   `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,  
  46.   PRIMARY KEY (`id`)  
  47. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表' AUTO_INCREMENT=1 ;  
  48. --  
  49. -- 轉存表中的資料 `ih_order`  
  50. --  
  51. -- --------------------------------------------------------  
  52. --  
  53. -- 表的結構 `ih_store`  
  54. --  
  55. CREATE TABLE IF NOT EXISTS `ih_store` (  
  56.   `id` int(11) NOT NULL AUTO_INCREMENT,  
  57.   `goods_id` int(11) NOT NULL,  
  58.   `sku_id` int(10) unsigned NOT NULL DEFAULT '0',  
  59.   `number` int(10) NOT NULL DEFAULT '0',  
  60.   `freez` int(11) NOT NULL DEFAULT '0' COMMENT '虛擬庫存',  
  61.   PRIMARY KEY (`id`)  
  62. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='庫存' AUTO_INCREMENT=2 ;  
  63. --  
  64. -- 轉存表中的資料 `ih_store`  
  65. --  
  66. INSERT INTO `ih_store` (`id`, `goods_id`, `sku_id`, `number`, `freez`) VALUES  
  67. (1, 1, 11, 500, 0);  

cmd中測試  apache中自帶的壓力測試器ab    apache/bin/ab.exe


模擬5000高併發測試
webbench -c 5000 -t 60 http://192.168.1.198/big/index.php
ab -r -n 6000 -c 5000  http://192.168.1.198/big/index.php

總結來說 ,解決高併發的所有方式

1.可以給語句加行鎖  但是這種方式會造成資料庫的壓力(壓力特別大)

2.因為超賣會是造成資料庫中的記錄變為負數   所以我們可以想到將這個欄位的資料型別設定為不能為負數   所以當超賣時會報錯因而阻止超賣  但是缺點是會報錯從而影響使用者的體驗  

3.使用檔案排他鎖

4.使用redis

5.可以使用rabbitMQ訊息對列

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

缺點是:如果出現超賣情況就會報錯對於使用者體驗來說不好

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


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

優化方案3:使用MySQL的事務,鎖住操作的行採取FIFO佇列思路(我們直接將請求放入佇列中,採用FIFO(First Input First Output,先進先出),這樣的話,我們就不會導致某些請求永遠獲取不到鎖。看到這裡,是不是有點強行將多執行緒變成單執行緒的感覺哈。


缺點:但是,系統處理完一個佇列內請求的速度根本無法和瘋狂湧入佇列中的數目相比。也就是說,佇列內的請求會越積累越多,最終Web系統平均響應時候還是會大幅下降,系統還是陷入異常。

優化方案4:使用非阻塞的檔案排他鎖(樂觀鎖思路這個資料所有請求都有資格去修改,但會獲得一個該資料的版本號,只有版本號符合的才能更新成功,其他的返回搶購失敗。這樣的話,我們就不需要考慮佇列的問題,不過,它會增大CPU的計算開銷。但是,綜合來說,這是一個比較好的解決方案。


缺點:會增大CPU的計算開銷

優化方案5:Redis中的watch