高併發業務場景下的秒殺解決方案(初探)

浪子編程走四方 做者:浪子編程走四方,勤記錄,懂分享,刻意練習,日精進! 公衆號:深夜有話聊php

文章簡介

本文內容是對併發業務場景出現超賣狀況而寫的一片解決方案。主要是利用到了 Redis 中的隊列技術。css

超賣介紹

所謂的超賣,就是咱們的售賣量大於了物品的庫存量。該狀況通常出如今電商系統中促銷類的業務場景中。輕則只是部分商品超賣,較小的經濟損失,可是當大量的超賣狀況,例如淘寶雙十一這樣的業務場景下致使超賣,則損失是很是大的,同時給用戶體驗帶來的也是負面影響,頗有可能損失用戶量。記得以前遇到一個公司,作電商項目,就是由於超賣致使公司倒閉。html

常規的秒殺模式

首先,咱們見下圖. jquery

1.第一步是咱們用戶進入商品秒殺頁面,點擊秒殺按鈕,向服務端發送秒殺請求。

2.服務端在接受到用戶秒殺請求,根據請求的商品id參數,去查詢數據庫中該商品id的庫存量。

3.當查詢到該商品庫存量後,進行判斷。若是庫存量不足,則返回給用戶,商品庫存不足的信息。

4.當查詢到該商品的庫存足夠時,則生成訂單數據並減小商品庫存。接着將成功信息返回給用戶。

5.用戶接受到搶購成功消息後,纔可進入下單頁面。此時按照正常邏輯,進行下單支付。

這種模式爲何會出現超賣呢?ajax

按照咱們上面所講的,按理來講是一種正常的邏輯流程。可是當並打量大的時候,就會出現超賣狀況。在上圖第 2 步驟中,是作商品庫存的查詢。假如此時咱們查詢到的商品庫存爲 1,這時候就會走 4 中上面的部分(插入搶購信息並減小庫存),因爲併發量大的狀況下,下一個請求在上一個還未執行減庫操做就去查詢了商品庫存,這時候查詢出來的庫存量依然是 1。一樣的,會走到 4 上面的步驟中去。而後上一個請求執行了減庫操做,此時庫存爲 0,第二個請求再去減庫時,就會把庫存量設置爲-1,這樣就出現了超賣狀況。因爲併發,同時會發生不少請求,所以減小的數量不單單是 1 了,或許是成百上千甚至上萬等等。redis

解決超賣思路

網上有不少這樣的思路,幾乎是經過<kbd>隊列技術</kbd>來解決的。先將商品庫存信息緩存到咱們的緩存中去,例如 Redis。(文章中示例也是經過該方案實現)。數據庫

秒殺實現

這裏單獨講一講示例代碼中秒殺的解決思路。編程

  1. 在秒殺前將商品的庫存信息加入到 Redis 緩存中。以下格式:
$redis->lpush('商品id',1);

當每個商品有多少個庫存則循環多少次,這樣就能夠保證每一個商品隊列中的長度就是商品庫存長度。<font color='red'>其實這裏我的是有一個疑問的,若是商品少,咱們加入到緩存的耗時是很小的,可是商品數量大,這樣就很耗時,而且 redis 是放在內存中的,也暫用大量的內存。</font>json

  1. 當秒殺開始時,用戶發送請求,每次去檢測一下商品的隊列是否爲空,當非空時,則使用 lpop 減小一個長度,也就是減小一個庫存量。這時候將秒殺的信息寫入到緩存中去,給緩存信息配一個惟一的鍵,將該鍵返回給用戶。(因爲 lpop 是原子性的,便是大量併發來了,也是要在 Redis 內部進行排隊執行的,假如在判斷是否爲空時,檢測到是非空,進行 lpop 操做,因爲隊列是空,這時候去執行出隊列也是返回錯誤的)。緩存

  2. 返回給用戶秒殺成功的信息,用戶根據返回的鍵進行下單操做。利用該鍵,將秒殺中的緩存信息寫入數據庫並生成對應的訂單。

接下來,咱們能夠結合上圖,得出下面的流程圖:

代碼具體實現

建立公共的 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);
/**
 * 生成訂單,訂單入庫
 *
 */

相關文章
相關標籤/搜索