高并发业务场景下的秒杀解决方案(初探)

浪子不回头ぞ 提交于 2019-11-29 08:36:33

浪子编程走四方 作者:浪子编程走四方,勤记录,懂分享,刻意练习,日精进! 公众号:深夜有话聊

文章简介

本文内容是对并发业务场景出现超卖情况而写的一片解决方案。主要是利用到了 Redis 中的队列技术。

超卖介绍

所谓的超卖,就是我们的售卖量大于了物品的库存量。该情况一般出现在电商系统中促销类的业务场景中。轻则只是部分商品超卖,较小的经济损失,但是当大量的超卖情况,例如淘宝双十一这样的业务场景下导致超卖,则损失是非常大的,同时给用户体验带来的也是负面影响,很有可能损失用户量。记得之前遇到一个公司,做电商项目,就是因为超卖导致公司倒闭。

常规的秒杀模式

首先,我们见下图.

1.第一步是我们用户进入商品秒杀页面,点击秒杀按钮,向服务端发送秒杀请求。

2.服务端在接受到用户秒杀请求,根据请求的商品id参数,去查询数据库中该商品id的库存量。

3.当查询到该商品库存量后,进行判断。如果库存量不足,则返回给用户,商品库存不足的信息。

4.当查询到该商品的库存足够时,则生成订单数据并减少商品库存。接着将成功信息返回给用户。

5.用户接受到抢购成功消息后,才可进入下单页面。此时按照正常逻辑,进行下单支付。

这种模式为什么会出现超卖呢?

按照我们上面所讲的,按理来说是一种正常的逻辑流程。但是当并打量大的时候,就会出现超卖情况。在上图第 2 步骤中,是做商品库存的查询。假如此时我们查询到的商品库存为 1,这时候就会走 4 中上面的部分(插入抢购信息并减少库存),由于并发量大的情况下,下一个请求在上一个还未执行减库操作就去查询了商品库存,这时候查询出来的库存量依然是 1。同样的,会走到 4 上面的步骤中去。然后上一个请求执行了减库操作,此时库存为 0,第二个请求再去减库时,就会把库存量设置为-1,这样就出现了超卖情况。由于并发,同时会发生很多请求,因此减少的数量不仅仅是 1 了,或许是成百上千甚至上万等等。

解决超卖思路

网上有很多这样的思路,几乎是通过<kbd>队列技术</kbd>来解决的。先将商品库存信息缓存到我们的缓存中去,例如 Redis。(文章中示例也是通过该方案实现)。

秒杀实现

这里单独讲一讲示例代码中秒杀的解决思路。

  1. 在秒杀前将商品的库存信息加入到 Redis 缓存中。如下格式:
$redis->lpush('商品id',1);

当每一个商品有多少个库存则循环多少次,这样就可以保证每个商品队列中的长度就是商品库存长度。<font color='red'>其实这里个人是有一个疑问的,如果商品少,我们加入到缓存的耗时是很小的,但是商品数量大,这样就很耗时,并且 redis 是放在内存中的,也暂用大量的内存。</font>

  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);
/**
 * 生成订单,订单入库
 *
 */

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!