高并發(fā)下用戶搶購(gòu)問(wèn)題簡(jiǎn)答
前言
面試題當(dāng)中如何處理高并發(fā)用戶搶購(gòu)問(wèn)題可以說(shuō)是一個(gè)十分經(jīng)典的問(wèn)題,經(jīng)常被提及,在這就這個(gè)問(wèn)題寫(xiě)一個(gè)簡(jiǎn)要的解答;
思路
并發(fā)的最大瓶頸永遠(yuǎn)是數(shù)據(jù)庫(kù),MySQL的讀寫(xiě)速度是制約并發(fā)的最大問(wèn)題,而搶購(gòu)之時(shí)真正需要寫(xiě)入的用戶量實(shí)際上是很少的,等于搶購(gòu)的商品總數(shù).這就要求我們需要把無(wú)效的用戶排除出.在前期放入有效用戶量,一旦產(chǎn)品搶購(gòu)結(jié)束,將活動(dòng)頁(yè)改為結(jié)束也的靜態(tài)頁(yè)面,最大程度的提升服務(wù)器相應(yīng)速度.這里面放入限定數(shù)量的用戶是最為關(guān)鍵的地方.
redis的高性能讀寫(xiě)是現(xiàn)在最為主流的解決方案.下面就簡(jiǎn)單的介紹一下如何用redis來(lái)完成高并發(fā)搶購(gòu)處理.
解決方案
將用戶id寫(xiě)入redis列表當(dāng)中,一旦列表長(zhǎng)度達(dá)到商品總數(shù),則拒掉后面的用戶.
示例代碼
Talk is cheap, show you my code.
<?php
/**
* Created by PhpStorm.
* User: mc
* Date: 18/3/21
* Time: 下午1:53
*/
$user_id = rand(100, 10000); // 模擬用戶id
$key = 'user_list'; // 列表名
$redis = new Redis();
$redis->pconnect('localhost', 6379);
$len = $redis->lLen($key); // 獲取列表長(zhǎng)度
$count = 10; // 商品總數(shù)
if ($len >= $count) { // 達(dá)到商品總數(shù)則停止搶購(gòu)
echo '秒殺已結(jié)束';
return '秒殺已結(jié)束';
}
$redis->lPush($key, $user_id); // 將用戶id推入列表
echo '恭喜你秒殺成功';
return '恭喜你秒殺成功';
ab測(cè)
ab -n 3000 -c 100 http://localhost:8001/ // 模擬3000個(gè)請(qǐng)求,100個(gè)并發(fā)
結(jié)果
列表當(dāng)中的結(jié)果確實(shí)是10條,滿足要求
LRANGE user_list 0 -1
1) "466"
2) "5090"
3) "8299"
4) "6436"
5) "4537"
6) "9617"
7) "9291"
8) "2162"
9) "1903"
10) "983"
Concurrency Level: 100
Time taken for tests: 4.558 seconds
Complete requests: 3000
Failed requests: 0
Total transferred: 540000 bytes
HTML transferred: 45000 bytes
Requests per second: 658.22 [#/sec] (mean)
Time per request: 151.924 [ms] (mean)
Time per request: 1.519 [ms] (mean, across all concurrent requests)
Transfer rate: 115.70 [Kbytes/sec] received
QPS能夠達(dá)到658.
問(wèn)題
上面的代碼看似沒(méi)問(wèn)題,可是要是老司機(jī)一定不難發(fā)現(xiàn)代碼當(dāng)中存在一個(gè)極大的漏洞.當(dāng)列表當(dāng)中有9個(gè)值,兩個(gè)用戶同時(shí)取得$len,那么這兩個(gè)用戶就會(huì)被同時(shí)寫(xiě)入列表當(dāng)中,這樣就會(huì)出現(xiàn)超賣的問(wèn)題.而且寫(xiě)列表的方式就需要取數(shù)據(jù)要在搶購(gòu)?fù)瓿芍?這顯然不合理.
這需要我們將搶購(gòu)判斷和用戶列表拆分開(kāi)來(lái),redis當(dāng)中的string有一一自增的api具有原子性,哪怕并發(fā)情況下也一定能夠保證自增.這能夠很好的服務(wù)于我們的需求.
代碼示例
$user_id = rand(100, 10000); // 模擬用戶id
$key = 'user_list'; // 列表名
$redis = new Redis();
$redis->connect('localhost', 6379);
$len = $redis->incr('count');
$count = 10; // 商品總數(shù)
if ($len > $count) { // 達(dá)到商品總數(shù)則停止搶購(gòu)
echo '秒殺已結(jié)束';
return '秒殺已結(jié)束';
}
$redis->lPush($key, $user_id); // 將用戶id推入列表
echo '恭喜你秒殺成功';
return '恭喜你秒殺成功';
這樣就可以避免商品超售了,而且列表的生產(chǎn)和消費(fèi)可以同步進(jìn)行,提升業(yè)務(wù)體驗(yàn).當(dāng)然真正的業(yè)務(wù)當(dāng)中復(fù)雜度遠(yuǎn)高于這里.
關(guān)于避免用戶重復(fù)搶的簡(jiǎn)略方案
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$user_id = rand(1, 15);
if (!$redis->hSetNx('seckill', 'test:' . $user_id, 1)) { // hSetNx函數(shù)當(dāng)key存在時(shí)會(huì)返回false,不存在時(shí)才會(huì)被設(shè)置成功,返回true
echo '您已經(jīng)參加過(guò)秒殺請(qǐng)勿重復(fù)參加';
return '您已經(jīng)參加過(guò)秒殺請(qǐng)勿重復(fù)參加';
}
$len = $redis->incr('count');
$count = 10; // 商品總數(shù)
if ($len > $count) { // 達(dá)到商品總數(shù)則停止搶購(gòu)
echo '秒殺已結(jié)束';
return '秒殺已結(jié)束';
}
$redis->lPush('user_id', $user_id);
echo '恭喜您,秒殺成功';
return '恭喜您,秒殺成功';
上面的QPS才600+而原生的PHP在我的電腦下應(yīng)該能到達(dá)3000+,這里面new Redis()創(chuàng)建redis連接耗費(fèi)了過(guò)多的資源
連接復(fù)用
<?php
$redis = new Redis();
$redis->pconnect('127.0.0.1', 6379); // redis自帶連接復(fù)用函數(shù)
$user_id = rand(1, 15);
if (!$redis->hSetNx('seckill', 'test:' . $user_id, 1)) { // hSetNx函數(shù)當(dāng)key存在時(shí)會(huì)返回false,不存在時(shí)才會(huì)被設(shè)置成功,返回true
echo '您已經(jīng)參加過(guò)秒殺請(qǐng)勿重復(fù)參加';
return '您已經(jīng)參加過(guò)秒殺請(qǐng)勿重復(fù)參加';
}
$len = $redis->incr('count');
$count = 10; // 商品總數(shù)
if ($len > $count) { // 達(dá)到商品總數(shù)則停止搶購(gòu)
echo '秒殺已結(jié)束';
return '秒殺已結(jié)束';
}
$redis->lPush('user_id', $user_id);
echo '恭喜您,秒殺成功';
return '恭喜您,秒殺成功';
Concurrency Level: 100
Time taken for tests: 0.428 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 180000 bytes
HTML transferred: 15000 bytes
Requests per second: 2337.77 [#/sec] (mean)
Time per request: 42.776 [ms] (mean)
Time per request: 0.428 [ms] (mean, across all concurrent requests)
Transfer rate: 410.94 [Kbytes/sec] received
結(jié)束
QPS能夠高達(dá)2300+完全能夠適應(yīng)一般業(yè)務(wù)的并發(fā)需求了.