文章簡介
本文內容是對併發業務場景出現超賣情況而寫的一片解決方案。主要是利用到了 Redis 中的佇列技術。
超賣介紹
所謂的超賣,就是我們的售賣量大於了物品的庫存量。該情況一般出現在電商系統中促銷類的業務場景中。輕則只是部分商品超賣,較小的經濟損失,但是當大量的超賣情況,例如淘寶雙十一這樣的業務場景下導致超賣,則損失是非常大的,同時給使用者體驗帶來的也是負面影響,很有可能損失使用者量。記得之前遇到一個公司,做電商專案,就是因為超賣導致公司倒閉。
常規的秒殺模式
首先,我們見下圖.
1.第一步是我們使用者進入商品秒殺頁面,點選秒殺按鈕,向服務端傳送秒殺請求。
2.服務端在接受到使用者秒殺請求,根據請求的商品id引數,去查詢資料庫中該商品id的庫存量。
3.當查詢到該商品庫存量後,進行判斷。如果庫存量不足,則返回給使用者,商品庫存不足的資訊。
4.當查詢到該商品的庫存足夠時,則生成訂單資料並減少商品庫存。接著將成功資訊返回給使用者。
5.使用者接受到搶購成功訊息後,才可進入下單頁面。此時按照正常邏輯,進行下單支付。
這種模式為什麼會出現超賣呢?
按照我們上面所講的,按理來說是一種正常的邏輯流程。但是當併發量大的時候,就會出現超賣情況。在上圖第 2 步驟中,是做商品庫存的查詢。假如此時我們查詢到的商品庫存為 1,這時候就會走 4 中上面的部分(插入搶購資訊並減少庫存),由於併發量大的情況下,下一個請求在上一個還未執行減庫操作就去查詢了商品庫存,這時候查詢出來的庫存量依然是 1。同樣的,會走到 4 上面的步驟中去。然後上一個請求執行了減庫操作,此時庫存為 0,第二個請求再去減庫時,就會把庫存量設定為-1,這樣就出現了超賣情況。由於併發,同時會發生很多請求,因此減少的數量不僅僅是 1 了,或許是成百上千甚至上萬等等。
解決超賣思路
網上有很多這樣的思路,幾乎是通過佇列技術來解決的。先將商品庫存資訊快取到我們的快取中去,例如 Redis。(文章中示例也是通過該方案實現)。
秒殺實現
這裡單獨講一講示例程式碼中秒殺的解決思路。
- 在秒殺前將商品的庫存資訊加入到 Redis 快取中。如下格式:
$redis->lpush('商品id',1);
當每一個商品有多少個庫存則迴圈多少次,這樣就可以保證每個商品佇列中的長度就是商品庫存長度。其實這裡個人是有一個疑問的,如果商品少,我們加入到快取的耗時是很小的,但是商品數量大,這樣就很耗時,並且 redis 是放在記憶體中的,也暫用大量的記憶體。
-
當秒殺開始時,使用者傳送請求,每次去檢測一下商品的佇列是否為空,當非空時,則使用 lpop 減少一個長度,也就是減少一個庫存量。這時候將秒殺的資訊寫入到快取中去,給快取資訊配一個唯一的鍵,將該鍵返回給使用者。(由於 lpop 是原子性的,即是大量併發來了,也是要在 Redis 內部進行排隊執行的,假如在判斷是否為空時,檢測到是非空,進行 lpop 操作,由於佇列是空,這時候去執行出佇列也是返回錯誤的)。
-
返回給使用者秒殺成功的資訊,使用者根據返回的鍵進行下單操作。利用該鍵,將秒殺中的快取資訊寫入資料庫並生成對應的訂單。
接下來,我們可以結合上圖,得出下面的流程圖:
程式碼具體實現
建立公共的 Redis 連線
<?php
/**
* Redis連線
*/
$redis = new Redis();
$result = $redis->connect('127.0.0.1',6379,2);
if(!$result){
die('redis connect fail');
}
秒殺前將商品庫存寫入快取中
/**
* 模擬商品庫存如佇列
*/
require_once __DIR__.'/redis_connect.php';
// 模擬資料庫查詢的商品資料
$goodsList = [
['id'=>1,'name'=>'夏季外套','price'=>12.32,'count'=>12],
['id'=>2,'name'=>'冬季外套','price'=>12.32,'count'=>1],
['id'=>3,'name'=>'秋季外套','price'=>12.32,'count'=>2],
['id'=>4,'name'=>'春季外套','price'=>12.32,'count'=>23],
['id'=>5,'name'=>'男士內衣','price'=>12.32,'count'=>8],
['id'=>6,'name'=>'男士馬甲','price'=>12.32,'count'=>180],
['id'=>7,'name'=>'男士長褲','price'=>12.32,'count'=>120],
];
// 將商品庫存新增到redis佇列中
$goodqueue = 'goods:queue:';
foreach($goodsList as $key => $val){
$count = $val['count'];
for($i=0;$i<$count;$i++){
$result = $redis->lpush($goodqueue.$val['id'],1);
echo $result.'<br/>';
}
}
模擬客戶傳送請求,這裡可以開多個視窗,增加請求量。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
模擬秒殺場景,使用者請求
<div class="content"></div>
<script src="https://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
<script>
// 簡單模擬1000個使用者傳送請求
for (let index = 0; index < 1000; index++) {
$.ajax({
type: "POST",
url: "http://localhost/Test/redis_miaosha.php",
data: {
userId: index,
goodsId: Math.floor(Math.random() * 10)
},
dataType: "json",
success: function(res) {
console.log(res.result);
if (res.result === "OK") {
$(".content").append(
"<a href='http://localhost/Test/redis_server.php?key=" +
res.key +
"' target='_blank'>使用者id為" +
index +
"的搶購成功!</a><br/>"
);
} else if (res.result === "FAIL") {
$(".content").append(
"<a href=''>使用者id為" +
index +
"的搶購失敗!</a><br/>"
);
}
}
});
}
</script>
</body>
</html>
服務端接收秒殺請求並寫入快取
<?php
/**
* 模擬使用者秒殺場景
*/
require_once __DIR__.'/redis_connect.php';
/**
*
* 1.接受使用者請求
* 2.驗證使用者是否已經參與秒殺,商品是否存在
* 3.根據商品id減少商品佇列中的庫存數量
* 4.將使用者的秒殺資料寫入server層中,並返回秒殺資料對應的唯一key值
* 5.使用者點選下單,根據serve層中的快取資料,生成訂單資料並減少資料庫商品的庫存資料
*/
$getParams = $_POST;
$userId = $getParams['userId'];
$goodsId = $getParams['goodsId'];
$key = 'goods:miaosha:';
$userResult = $redis->get($key.$userId);
if($userResult){
$userResult = json_decode($userResult,true);
echo json_encode(['result'=>$userResult['result'],'key'=>$key.$userId]);// 已經參與過秒殺了
die();
}else{
$goodqueue = 'goods:queue:'.$goodsId;
$result = $redis->lpop($goodqueue);// 刪除商品redis佇列快取
if($result){
$data = json_encode(['result'=>'OK','userId'=>$userId,'goodsId'=>$goodsId]);
$redis->set($key.$userId,$data);// 將秒殺資訊寫入快取中
echo json_encode(['result'=>'OK','userId'=>$userId,'goodsId'=>$goodsId,'key'=>$key.$userId]);
die();
}else{
echo json_encode(['result'=>'FAIL','message'=>'商品不存在','goodsId'=>$goodsId]);// 商品庫存不存在
die();
}
}
客戶端在接收到秒殺請求結果後,進行支付
<?php
/**
* 使用者下單介面
*/
require_once __DIR__.'/redis_connect.php';
$key = $_GET['key'];
$data = $redis->get($key);
/**
* 生成訂單,訂單入庫
*
*/