原文:https://my.oschina.net/chinaxy/blog/1829233
搶購是如今很常見的一個應用場景,主要需要解決的問題有兩個:
1 高並發對數據庫產生的壓力
2 競爭狀態下如何解決庫存的正確減少(“超賣”問題)
對於第一個問題,已經很容易想到用緩存來處理搶購,避免直接操作數據庫,例如使用Redis。重點在於第二個問題,我們看看下面一種常規的實現代碼:
<?php require('predis/src/Autoloader.php'); $redis = new Predis\Client(array( 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => '6379' )); //redis 登錄 $redis->auth('123456'); //庫存 $num = 10; //用戶id $user_id = $_SESSION['user_id']; //檢查庫存 $len = $redis->llen('order:1'); if($len >= $num){ exit('已經搶光了'); } //把搶到的用戶存入到列表中 $result = $redis->lpush('order:1',$user_id); if($result){ echo '搶到了'; } ?>
如果代碼正常運行,列表order:1中最多只能存儲10個用戶的id,因為庫存只有10個。
然而,在使用Apache AB工具模擬很多用戶並發請求時,最后發現order:1中總是超過10個用戶,也就是出現了“超賣”。
問題就出在這一段代碼:
//檢查庫存 $len = $redis->llen('order:1'); if($len >= $num){ exit('已經搶光了'); }
在搶購進行到一定程度,假如現在已經有9個人搶購成功,又來了3個用戶同時搶購,這時if條件將會被繞過,這三個用戶都能搶購成功。而實際上只有一件庫存可以搶了。
在高並發下,很多不是問題的,都成了問題。要解決“超賣”問題,核心在於保證檢查庫存時的操作是依次執行的,形象的說就是把“多線程”轉成“單線程”。即使有很多用戶同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,后面的用戶就無法繼續了。
我們需要使用Redis的原子操作來實現這個“單線程”。首先我們把庫存存在goods:1這個列表中,假設有10件庫存,就往列表中push10個數,這個數沒有實際意義,僅僅代表一件庫存。搶購開始后,每到來一個用戶,就從goods:1中pop一個數,表示用戶搶購成功。當列表為空時,表示已經被搶光了。因為列表的pop操作是原子的,即使有很多用戶同時到達,也是依次執行的。搶購的示例代碼如下:
<?php //搶購 require('predis/src/Autoloader.php'); $redis = new Predis\Client(array( 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => '6379' )); $redis->auth('123456'); //用戶ID $user_id = $_SESSION['user_id']; $check = $redis->lpop('goods:1'); if(!$check){ exit('搶光了'); } $result = $redis->lpush('order:1',$user_id); if($result){ echo '搶購成功'; } ?>
用戶搶購成功后,我們將用戶ID存入了order:1列表中。接下來我們可以引導這些用戶去完成訂單的其他步驟,這里才涉及到與數據庫的交互。最終只有很少的人走到這一步,也就解決的數據庫的壓力問題。
為了檢測實際效果,我使用Apache AB工具模擬10、20、1000個用戶並發進行搶購,經過大量的測試,最終搶購成功的用戶始終為10,沒有出現“超賣”。