关于抢购秒杀的实现思路与事例代码
#事先說明,本次的文章所貼的事例代碼并非本人,具體出自什么地方?我也無從考究。不過今天要為大家講的就是基于這些事例代碼結合對應的個人理解進行分析。如果有什么覺得說得不正確的請各位看官拍磚。也讓我學而知不足。
#關于秒殺搶購的思路一般都基于三個部分進行設計
1.用戶頁面層,這個部分可以設置頁面緩存,cdn加速,適當的請求攔截。當然前兩者相信各位很容易理解,那什么是請求攔截了?其實說白了就是當用戶點擊了提交按鈕后,記得通過ajax把按鈕設置為禁用狀態。須知道用戶在煩躁的時候可是會瘋狂地點擊提交按鈕,這部分的請求如果你不過濾到那豈不是在白白浪費服務器的資源?
2.數據接入層,在數據接入層的這個層面來說我們一般我們就要對用戶的請求進行判斷,盡量把惡意的請求都拒絕在外,常見的做法就是同一個IP在限定的時間段內限制訪問次數,或者通過記錄用戶的UID來限制用一個用戶的UID在每分鐘的請求次數,用來過濾一些高端用戶通過腳本來參與請求的。
3.數據處理層,最后我們本次文章就是要基于數據處理層的代碼展示來為大家說一下關于搶購的處理思路。其實對于搶購和秒殺的核心處理思路就是防止超賣,還有防止服務器迅時流量的爆增導致服務的崩潰。
那么我們先看一個傳統的搶購流程
上面這個例子,假設某個搶購場景中,我們一共只有100個商品,在最后一刻,我們已經消耗了99個商品,僅剩最后一個。這個時候,系統發來多個并發請求,這批請求讀取到的商品余量都是99個,然后都通過了這一個余量判斷,最終導致超發。在上面的這個圖中,就導致了并發用戶B也“搶購成功”,多讓一個人獲得了商品。這種場景,在高并發的情況下非常容易出現。
優化方案1:將庫存字段number字段設為unsigned,當庫存為0時,因為字段不能為負數,將會返回false
當然上述的優化還是不夠的,接下來我們要進行的另一個優化方式就是往悲觀鎖去考慮,什么是悲觀鎖呢?其實就是在修改數據的時候,采用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。
優化方案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); } //模擬下單操作 //庫存是否大于0 mysqli_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"); } ?> 復制代碼雖然上述的方案的確解決了線程安全的問題,但是,別忘記,我們的場景是“高并發”。也就是說,會很多這樣的修改請求,每個請求都需要等待“鎖”,某些線程可能永遠都沒有機會搶到這個“鎖”,這種請求就會死在那里。同時,這種請求會很多,瞬間增大系統的平均響應時間,結果是可用連接數被耗盡,系統陷入異常。
因此我們就可以采用一種非阻塞模式文件鎖的方式來解決這個問題。首先在貼代碼之前你可能會問什么是非阻塞呢?簡單來說說,文件鎖可以分為兩種模式,一種是阻塞文件鎖,另一種是非阻塞文件鎖。阻塞文件鎖,會當文件被占用的時候,其他用戶無法打開文件且一直在等待過程。而非阻塞文件鎖呢,文件在被占用時,可以直接返回false給用戶,從而節省用戶的等待時間。
優化方案3:非阻塞文件排他鎖方式
<?php##注意進入隊列的操作這里沒有//優化方案3:使用非阻塞的文件排他鎖 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);?> 復制代碼對于日IP不高或者說并發數不是很大的應用,用一般的文件操作方法完全沒有問題。但如果并發高,在我們對通過使用文件鎖操作其實是非常消耗性能的。因此我們可以引入新的思路。
4. FIFO隊列思路
那好,那么我們稍微修改一下上面的場景,我們直接將請求放入隊列中的,采用FIFO(First Input First Output,先進先出),當然這里的隊列我們要使用我們耳熟能詳的redis隊列。
優化思路4:通過引入隊列的方式
#先將商品庫存如隊列<?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'); 復制代碼#數據處理 <?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); }//模擬下單操作 //下單前判斷redis隊列庫存量 $redis=new Redis(); $result=$redis->connect('127.0.0.1',6379); $count=$redis->lpop('goods_store'); if(!$count){insertLog('error:no store redis');return; }//生成訂單 $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('庫存減少失敗'); } 復制代碼那么新的問題來了,高并發的場景下,因為請求很多,很可能一瞬間將隊列內存“撐爆”,然后系統又陷入到了異常狀態?;蛘咴O計一個極大的內存隊列,也是一種方案,但是,系統處理完一個隊列內請求的速度根本無法和瘋狂涌入隊列中的數目相比。也就是說,隊列內的請求會越積累越多,最終Web系統平均響應時候還是會大幅下降,系統還是陷入異常。
這個時候,我們就可以討論一下“樂觀鎖”的思路了。樂觀鎖,是相對于“悲觀鎖”采用更為寬松的加鎖機制,大都是采用帶版本號(Version)更新。實現就是,這個數據所有請求都有資格去修改,但會獲得一個該數據的版本號,只有版本號符合的才能更新成功,其他的返回搶購失敗。這樣的話,我們就不需要考慮隊列的問題,不過,它會增大CPU的計算開銷。但是,綜合來說,這是一個比較好的解決方案。
有很多軟件和服務都“樂觀鎖”功能的支持,例如Redis中的watch就是其中之一。通過這個實現,我們保證了數據的安全。
#到此,關于搶購秒殺的應用優化思路暫時告一段落。如果上述理解有誤請各位留言提供你們的思路,或者你們認為更好的方法讓我學習下。謝謝
總結
以上是生活随笔為你收集整理的关于抢购秒杀的实现思路与事例代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【转】数学专业参考书整理推荐V3.0版
- 下一篇: 修复好一个alpha865qqz的勒索病