浪子编程走四方 做者:浪子编程走四方,勤记录,懂分享,刻意练习,日精进! 公众号:深夜有话聊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。(文章中示例也是经过该方案实现)。数据库
这里单独讲一讲示例代码中秒杀的解决思路。编程
$redis->lpush('商品id',1);
当每个商品有多少个库存则循环多少次,这样就能够保证每一个商品队列中的长度就是商品库存长度。<font color='red'>其实这里我的是有一个疑问的,若是商品少,咱们加入到缓存的耗时是很小的,可是商品数量大,这样就很耗时,而且 redis 是放在内存中的,也暂用大量的内存。</font>json
当秒杀开始时,用户发送请求,每次去检测一下商品的队列是否为空,当非空时,则使用 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); /** * 生成订单,订单入库 * */