搶購是現在很常見的一個應用場景,主要須要解決的問題有兩個: php
1 高併發對數據庫產生的壓力 redis
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個用戶,也就是出現了「超賣」。 併發
問題就出在這一段代碼: tcp
//檢查庫存 $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工具模擬十、20、1000個用戶併發進行搶購,通過大量的測試,最終搶購成功的用戶始終爲10,沒有出現「超賣」。