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

浪子编程走四方 做者:浪子编程走四方,勤记录,懂分享,刻意练习,日精进! 公众号:深夜有话聊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);
/**
 * 生成订单,订单入库
 *
 */

相关文章
相关标签/搜索