搶購(gòu)是如今很常見(jiàn)的一個(gè)應(yīng)用場(chǎng)景,主要需要解決的問(wèn)題有兩個(gè):
1 高并發(fā)對(duì)數(shù)據(jù)庫(kù)產(chǎn)生的壓力
2 競(jìng)爭(zhēng)狀態(tài)下如何解決庫(kù)存的正確減少(“超賣(mài)”問(wèn)題)
對(duì)于第一個(gè)問(wèn)題,已經(jīng)很容易想到用緩存來(lái)處理?yè)屬?gòu),避免直接操作數(shù)據(jù)庫(kù),例如使用Redis。重點(diǎn)在于第二個(gè)問(wèn)題,我們看看下面一種常規(guī)的實(shí)現(xiàn)代碼:
require('predis/src/Autoloader.php');
$redis = new Predis\Client(array(
'scheme' => 'tcp',
'host'? => '127.0.0.1',
'port'? => '6379'
));
//redis 登錄
$redis->auth('123456');
//庫(kù)存
$num = 10;
//用戶id
$user_id = $_SESSION['user_id'];
//檢查庫(kù)存
$len = $redis->llen('order:1');
if($len >= $num){
exit('已經(jīng)搶光了');
}
//把搶到的用戶存入到列表中
$result = $redis->lpush('order:1',$user_id);
if($result){
echo '搶到了';
}
?>
如果代碼正常運(yùn)行,列表order:1中最多只能存儲(chǔ)10個(gè)用戶的id,因?yàn)閹?kù)存只有10個(gè)。
然而,在使用Apache AB工具模擬很多用戶并發(fā)請(qǐng)求時(shí),最后發(fā)現(xiàn)order:1中總是超過(guò)10個(gè)用戶,也就是出現(xiàn)了“超賣(mài)”。
問(wèn)題就出在這一段代碼:
//檢查庫(kù)存
$len = $redis->llen('order:1');
if($len >= $num){
exit('已經(jīng)搶光了');
}
在搶購(gòu)進(jìn)行到一定程度,假如現(xiàn)在已經(jīng)有9個(gè)人搶購(gòu)成功,又來(lái)了3個(gè)用戶同時(shí)搶購(gòu),這時(shí)if條件將會(huì)被繞過(guò),這三個(gè)用戶都能搶購(gòu)成功。而實(shí)際上只有一件庫(kù)存可以搶了。
在高并發(fā)下,很多不是問(wèn)題的,都成了問(wèn)題。要解決“超賣(mài)”問(wèn)題,核心在于保證檢查庫(kù)存時(shí)的操作是依次執(zhí)行的,形象的說(shuō)就是把“多線程”轉(zhuǎn)成“單線程”。即使有很多用戶同時(shí)到達(dá),也是一個(gè)個(gè)檢查并給與搶購(gòu)資格,一旦庫(kù)存搶盡,后面的用戶就無(wú)法繼續(xù)了。
我們需要使用Redis的原子操作來(lái)實(shí)現(xiàn)這個(gè)“單線程”。首先我們把庫(kù)存存在goods:1這個(gè)列表中,假設(shè)有10件庫(kù)存,就往列表中push10個(gè)數(shù),這個(gè)數(shù)沒(méi)有實(shí)際意義,僅僅代表一件庫(kù)存。搶購(gòu)開(kāi)始后,每到來(lái)一個(gè)用戶,就從goods:1中pop一個(gè)數(shù),表示用戶搶購(gòu)成功。當(dāng)列表為空時(shí),表示已經(jīng)被搶光了。因?yàn)榱斜淼膒op操作是原子的,即使有很多用戶同時(shí)到達(dá),也是依次執(zhí)行的。搶購(gòu)的示例代碼如下:
//搶購(gòu)
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 '搶購(gòu)成功';
}
?>
用戶搶購(gòu)成功后,我們將用戶ID存入了order:1列表中。接下來(lái)我們可以引導(dǎo)這些用戶去完成訂單的其他步驟,這里才涉及到與數(shù)據(jù)庫(kù)的交互。最終只有很少的人走到這一步,也就解決的數(shù)據(jù)庫(kù)的壓力問(wèn)題。
為了檢測(cè)實(shí)際效果,我使用Apache AB工具模擬10、20、1000個(gè)用戶并發(fā)進(jìn)行搶購(gòu),經(jīng)過(guò)大量的測(cè)試,最終搶購(gòu)成功的用戶始終為10,沒(méi)有出現(xiàn)“超賣(mài)”。