NoSQL(Redis)秒杀
概念
秒杀
说明:秒杀就是指商家的限时大甩卖(商家为了售卖商品所采取的一种销售手段)
特征:1-限时,2-低价
种类:一元秒杀、低价限量秒杀、低价限时限量秒杀
好处:因为秒杀产品参与者数量众多,可以瞬间聚集人气,提升品牌影响力,是一种不错的促销手段。
并发
生活中:指同时有n个用户一起去收营台结账的表现可以称之为并发
网络中:指同时有n个用户一起访问网站的表现可以称之为并发
并发导致的问题:
生活中-忙不过来,程序中-服务器可能崩溃或者出现意外结果(负库存)
在计算机中:通过消息队列实现
MySQL负库存(秒杀可能出现的问题)
修改mysql.ini max_connections = 10 #声明同时支持多少个用户连接
打开 navicat.exe 执行sql语句
create database if not exists miaosha charset=utf8;
use miaosha;
create table goods (id int primary key auto_increment,num int) engine=innodb;
insert into goods values (null, 100);
在站点目录下创建mysql.php输入下述命令
<?php
#语法:ab -n 1000 -c 100 请求地址
#说明:n-请求总数, c- 每次请求量
//1.创建PDO对象
$pdo = new PDO('mysql:dbname=miaosha', 'root', 'root');
//2.查询库存
$pdoStatement = $pdo->query('select num from goods where id = 1');
$res = $pdoStatement->fetch(PDO::FETCH_ASSOC);
$num = $res['num'];
//3.判断库存
if ($num) {
//减库存
$pdo->exec('update goods set num=num-1 where id = 1');
echo '抢购成功';
} else {
echo '对不起,你来晚了,库存不足';
}
通过本地Apache安装目录下bin目录中的ab测压工具测试并发
ab.exe -n 1000 -c 100 http://127.0.0.1/mysql.php
多测试几次 查看数据库 可能会出现负库存 这些是并发量高,数据处理不过来,当前面用户下单时,后面用户也读取到了库存数据 就会出现负库存
Redis消息队列(解决秒杀问题)
使用Workerman框架
下载Workerman框架....
在站点目录下创建testworkerman.php输入手册中的定时器代码
<?php
use \Workerman\Worker;
use \Workerman\Lib\Timer;
require_once __DIR__ . '/Workerman/Autoloader.php';
$task = new Worker();
// 开启多少个进程运行定时任务,注意业务是否在多进程有并发问题
$task->count = 1;
$task->onWorkerStart = function($task)
{
// 每2.5秒执行一次
$time_interval = 2.5;
Timer::add($time_interval, function()
{
echo "task run\n";
});
};
// 运行worker
Worker::runAll();
打开DOS窗口通过php.exe执行testworkerman.php文件查看效果
实现
登录redis设置存放商品秒杀数据信息
flushall
hmset goods_seckill_1 start_time 0 stop_time 0 price 30 real_num 3 seckill_num 3
hmset goods_seckill_2 start_time 0 stop_time 0 price 30 real_num 2 seckill_num 2
在站点目录下创建redis.php输入下述命令
<?php
#步骤1:接受数据
$uid = 1;
$goods_id = 1;
#步骤2:连接Redis
$redis = new Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$redis->select(0);
#步骤3:过滤(判断时间和库存)
//获取商品信息
$goodsInfo = $redis->hmget("goods_seckill_{$goods_id}"., array(
'start_time', 'stop_time', 'real_num', 'seckill_num', 'price'
));
//判断是否开始
//判断是否结束
//判断库存
if ($goodsInfo['seckill_num'] < 1) {
echo json_encode(array('state' => 0, '对不起,宝贝已被抢完!'));
die;
}
#步骤4:将用户请求加入消息队列中
$len = $redis->lpush("goods_seckill_{$goods_id}_rs", $uid.'%'.$goods_id.'%'.$goodsInfo['price']);
#步骤5:判断库存(规则:队列中前n个抢购成功)
if ($len > $goodsInfo['real_num']) {
//抢购失败(队列长度 > 库存)
echo json_encode(array('state' => 0, '对不起,宝贝已被抢完!'));
die;
} else {
//抢购成功,减库存(注:千万不能直接操作mysql因为有并发限制)
echo json_encode(array('state' => 1, '秒杀成功'));
die;
}
在站点目录创建workerman.php输入下述命令
<?php
use \Workerman\Worker;
use \Workerman\Lib\Timer;
require_once __DIR__ . '/Workerman/Autoloader.php';
$task = new Worker();
// 开启多少个进程运行定时任务,注意业务是否在多进程有并发问题
$task->count = 1;
$task->onWorkerStart = function($task)
{
//每0.1秒执行一次(精度可以达到毫秒0.001)
$time_interval = 0.1;
Timer::add($time_interval, function()
{
$goods_id = 1;
#步骤1:连接Redis
$redis = new Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$redis->select(0);
#步骤2:获取秒杀相关信息
$allowBuyNum = $redis->hget("goods_seckill_{$goods_id}", 'seckill_num'); //秒杀剩余库存
$orderInfoString = $redis->rpop("goods_seckill_{$goods_id}_rs"); //队列抢购用户信息
#步骤3:判断(有库存 && 有人抢购)
if($allowBuyNum > 0 && $orderInfoString)
{
echo "allowBuyNum:$allowBuyNum\n";
#步骤4:减库存
$redis->hincrby('goods_seckill_1', 'seckill_num', -1);
#步骤5:生成订单
$pdo = new \PDO('mysql:dbname=php15shop', 'root', 'root');
$userOrderInfo = explode('%', $orderInfoString); //$uid.'%'.$goods_id.'%'.$price
$order_id = date('Ymd').time().uniqid();
$total_price = $userOrderInfo[2];
$member_id = $userOrderInfo[0];
$goods_id = $userOrderInfo[1];
$create_time = time();
$update_time = time();
#主表(sh_order)
$sql = "insert into sh_order (order_id, total_price, member_id, create_time, update_time)
value
('{$order_id}', $total_price, $member_id, $create_time, $update_time)";
$pdo->exec($sql);
#从表(sh_order_goods)
$sql = "insert into sh_order_goods (order_id, goods_id, goods_number, goods_price) value('{$order_id}', $goods_id , $total_price, 2)";
$pdo->exec($sql);
echo "over...\n";
}
});
};
// 运行worker
Worker::runAll();
通过DOS窗口运行workerman.php文件,监听队列数据
通过本地Apache安装目录下的bin目录ab测压工具测试并发
ab -n 1000 -c 100 http://127.0.0.1/redis2/redis.php
查看主表数据...
搭建秒杀项目虚拟主机
创建虚拟目录seckill
将seckill项目解压到站点目录中
打开seckill站点目录并修改数据库信息
将之前的shop商城数据复制一份 创建新数据库并修改sh_goods表增加字段is_seckill(是否秒杀商品)
在Admin后台创建Goods控制器seckillConfig方法
//商品秒杀配置
public function seckillConfig()
{
#步骤2:加载视图
return $this->fetch('');
}
创建视图文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>商品秒杀配置</title>
</head>
<body>
<form action="" method="post">
<p>秒杀开始时间<input type="text" name="start_time" /></p>
<p>秒杀结束时间<input type="text" name="stop_time" /></p>
<p>秒杀价格<input type="text" name="price" /></p>
<p>秒杀数量<input type="text" name="num" /></p>
<p><input type="submit" value="提交"></p>
</form>
</body>
</html>
数据处理:修改商品状态为秒杀 将秒杀商品数据保存的redis中
redis键规则:
hmset goods_seckill_1 start_time 0 stop_time 0 price 30 real_num 3 seckill_num 3
<a href="{:url('admin/goods/seckillConfig', array('goods_id' => $list['goods_id']))}"
class="showContent tablelink">
秒杀商品配置
</a>
修改admin后台的goods控制器sekillconfig方法进行数据处理
public function seckillConfig()
{
#步骤1:判断是否post提交
if (request()->isPost()) {
#步骤2:接受数据
$start_time = input('start_time');
$stop_time = input('stop_time');
$price = input('price');
$num = input('num');
$goods_id = input('goods_id');
#步骤3:插入数据
$redis = new \Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
#hmset 键 字段1 值1 ... 字段n 值n
#hmset goods_seckill_1 start_time 0 stop_time 0 price 30 real_num 3 seckill_num 3
$tempData = array(
'start_time' => $start_time,
'stop_time' => $stop_time,
'price' => $price,
'real_num' => $num,
'seckill_num' => $num,
);
$rs = $redis -> hMset('goods_seckill_'.$goods_id, $tempData);
#步骤4:判断
if ($rs) {
#修改商品状态为秒杀
Goods::where('goods_id', $goods_id)->update([
'is_seckill' => 1
]);
#跳转到商品秒杀列表页
$this->success("商品秒杀配置成功", url("admin/goods/seckill"));
}else{
$this->error("商品秒杀配置失败");
}
} else {
#步骤2:加载视图
return $this->fetch('');
}
}
整合日期插件
下载jq插件包放到查念public/plugin目录好
商品秒杀页配置引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>商品秒杀配置</title>
</head>
<body>
<form action="" method="post">
<p>秒杀开始时间
<input type="text" name="start_time"
onClick="WdatePicker({el:this,dateFmt:'yyyy-MM-dd HH:mm:ss'})"
autocomplete="off"
/>
</p>
<p>
秒杀结束时间<input type="text" name="stop_time"
onClick="WdatePicker({el:this,dateFmt:'yyyy-MM-dd HH:mm:ss'})"
autocomplete="off"
/>
</p>
<p>秒杀价格<input type="text" name="price" /></p>
<p>秒杀数量<input type="text" name="num" /></p>
<p><input type="submit" value="提交"></p>
</form>
<script language="javascript" type="text/javascript" src="/plugin/My97DatePicker/WdatePicker.js"></script>
</body>
</html>
修改控制器方法格式化日期
start_time => strtltime($start_time)
stop_time => strtotime($stop_time)
在Admin后台创建Goods控制器seckill方法
public function seckill()
{
#步骤1:查询所有数据
$seckills = Goods::where('is_seckill', 1)->select();
#步骤2:过滤数据
foreach ($seckills as $seckill) {
#$seckill->goods_id
#$seckill->goods_name
#查询商品秒杀信息
$redis = new \Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$temp = $redis -> hMget('goods_seckill_'.$seckill->goods_id, array(
'start_time',
'stop_time',
'price',
'real_num'
));
#将商品秒杀信息添加到$seckill中
$seckill->start_time = $temp['start_time'];
$seckill->stop_time = $temp['stop_time'];
$seckill->price = $temp['price'];
$seckill->real_num = $temp['real_num'];
}
#步骤3:加载视图
return $this->fetch('', [
'seckills'=>$seckills
]);
}
创建视图并循环显示数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<table border="1" cellpadding="10" cellspacing="0">
<tr>
<td>商品ID</td>
<td>商品名称</td>
<td>商品价格</td>
<td>商品数量</td>
<td>开始时间</td>
<td>结束时间</td>
<td>距离结束</td>
</tr>
{foreach $seckills as $seckill}
<tr>
<td>{$seckill.goods_id}</td>
<td>{$seckill.goods_name}</td>
<td>{$seckill.price}</td>
<td>{$seckill.real_num}</td>
<td>{:date('Y-m-d H:i:s', $seckill.start_time)}</td>
<td>{:date('Y-m-d H:i:s', $seckill.stop_time)}</td>
<td>0</td>
</tr>
{/foreach}
</table>
</body>
</html>
距离倒计时(修改控制器 ) 增加字段
$seckill->time = $temp['stop_time']
距离倒计时(修改视图)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="{:config('admin_static')}/js/jquery.js"></script>
<script>
function timer(intDiff,idName) {
window.setInterval(function() {
var day = 0,
hour = 0,
minute = 0,
second = 0; //时间默认值
if (intDiff > 0) {
day = Math.floor(intDiff / (60 * 60 * 24));
hour = Math.floor(intDiff / (60 * 60)) - (day * 24);
minute = Math.floor(intDiff / 60) - (day * 24 * 60) - (hour * 60);
second = Math.floor(intDiff) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60);
}
if (minute <= 9) minute = '0' + minute;
if (second <= 9) second = '0' + second;
$(idName).html(day + "天" + hour + '时' + minute + '分' + second + '秒');
//console.log(idName);
//$(idName+' .day_show').html(day + "天");
//$(idName+' .hour_show').html('<s id="h"></s>' + hour + '时');
//$(idName+' .minute_show').html('<s></s>' + minute + '分');
//$(idName+' .second_show').html('<s></s>' + second + '秒');
intDiff--;
}, 1000);
}
</script>
</head>
<body>
<table border="1" cellpadding="10" cellspacing="0">
<tr>
<td>商品ID</td>
<td>商品名称</td>
<td>商品价格</td>
<td>商品数量</td>
<td>开始时间</td>
<td>结束时间</td>
<td>距离结束</td>
</tr>
{foreach $seckills as $seckill}
<tr>
<td>{$seckill.goods_id}</td>
<td>{$seckill.goods_name}</td>
<td>{$seckill.price}</td>
<td>{$seckill.real_num}</td>
<td>{:date('Y-m-d H:i:s', $seckill.start_time)}</td>
<td>{:date('Y-m-d H:i:s', $seckill.stop_time)}</td>
<td id="time{$seckill.goods_id}">0</td>
</tr>
<script>
timer({$seckill->time},'#time{$seckill.goods_id}');
</script>
{/foreach}
</table>
</body>
</html>
完成前台秒杀功能
修改home/index/index
<li><a href="{:url('home/seckill/index')}">商品秒杀</a></li>
在后台创建seckill控制器index方法
<?php
namespace app\home\controller;
use think\Controller;
use app\home\model\Goods;
class SeckillController extends Controller
{
public function index()
{
#步骤1:查询所有数据
$seckills = Goods::where('is_seckill', 1)->select();
#步骤2:过滤数据
foreach ($seckills as $seckill) {
#$seckill->goods_id
#$seckill->goods_name
#查询商品秒杀信息
$redis = new \Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$temp = $redis -> hMget('goods_seckill_'.$seckill->goods_id, array(
'start_time',
'stop_time',
'price',
'real_num',
'seckill_num',
));
#将商品秒杀信息添加到$seckill中
$seckill->start_time = $temp['start_time'];
$seckill->stop_time = $temp['stop_time'];
$seckill->price = $temp['price'];
$seckill->real_num = $temp['real_num'];
$seckill->seckill_num = $temp['seckill_num'];
}
#步骤3:加载视图
return $this->fetch('', [
'seckills'=>$seckills
]);
}
}
创建视图并循环显示数据
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>一元秒杀!还包邮!</title>
<style type="text/css">
*{margin:0; padding:0;}
body{font-family:"微软雅黑";}
.html{ overflow:auto}
.c{ clear:both;}
a{text-decoration: NONE; color:#000; font-size:16px;}
.bodybg{ background:url({:config('home_static')}/image/bg.jpg); width:100%;}
.banner{ background:url({:config('home_static')}/image/banner.jpg) no-repeat center; height:542px;}
.nav{ border:none;width:1000px;margin:0 auto;}
.probg{ background:#f4ad1e; width:1000px; margin:0 auto; text-align:center;}
.fl{ float:left; padding-left:9px; padding-top:9px;}
.onebg{ width:321px; height:321px; background:#FFF;}
.titlebg{ width:321px; height:33px; background:#FFF;}
.buybg{ background:url({:config('home_static')}/image/pricebg.jpg); width:321px; height:70px;}
.foreprice{ font-size:17px; text-decoration:line-through; color:#FFF;float:left; padding-top:13px; padding-left:25px;}
.button{ width:96px;height:70px;float:right; padding:0;}
.ads{ width:1000px; height:326px; margin:0 auto;}
.sm{ background:#FFF; width:100%; margin:0 auto;}
.xize{ width:1000px; margin:0 auto; text-align:left; font-size:36px; font-weight:700; color:#9E051A;}
.shuoming{ width:1000px; margin:0 auto; text-align:left; font-size:20px;color:#9E051A; line-height:35px;}
</style>
</head>
<body>
<div class="banner"></div>
<div class="bodybg">
<div class="nav"><img src="{:config('home_static')}/image/nav.jpg" width="1000" height="70"/></div>
<div class="probg">
{foreach $seckills as $seckill}
<!-- 单个商品循环-->
<div class="fl">
<div class="onebg" >
<a href="javascript:void(0);" ><img src="{:config('home_static')}/image/1.jpg" width="321px;height:321px"/></a>
</div>
<div class="titlebg">
<a href="javascript:void(0);" >{$seckill.goods_name}</a>
</div>
<div class="buybg" style="background: red;">
<div class="foreprice" style="text-decoration:none;">价格:{$seckill.price}</div>
<div class="foreprice" style="text-decoration:none;">库存:{$seckill.seckill_num}</div>
<div class="button">
<!-- <img src="{:config('home_static')}/image/button.jpg"/> -->
<input type="button" value="立即抢购" style="margin-top:20px; width: 80px;height: 30px; cursor: pointer;" data-id="{$seckill.goods_id}"/>
</div>
</div>
</div>
<!-- 单个商品循环结束-->
{/foreach}
<div>
<a href="javascript:void(0);" ><img src="{:config('home_static')}/image/ads.jpg" /></a>
</div>
<div class="c"></div>
</div>
<div class="sm">
<br />
<div class="xize">1元秒杀细则:</div>
<div class="shuoming">1.参与秒杀前,请详细阅读秒杀规则,凡参与1元秒杀活动的用户,均视为同意秒杀规则。<br />2.秒杀商品将于2014年7月7日08:00:00上线-2014年7月11日23:59:59结束,当天商品售罄时当天秒杀结束,活动期间每一个云中央注册会员每期仅限秒杀一个商品,秒杀多件成功者,并通过收货人及联系方式可判定为同一人的,则取消全部订单。<br />3.秒杀成功以支付成功为准,早秒早得;秒杀下单后30分钟内未付款者自动取消订单,请特别注意。<br />4.请确保秒杀填写的收货人信息真实有效,因联系方式填写错误导致未收到礼品的,由用户自行承担损失。<br />5.对于任何通过不正当手段参与秒杀者,不正当手段包括但不限于使用秒杀器或类似作弊软件,云中央网站有权依据自身技术判断,并在不事先通知的情况下取消其秒杀资格或者取消订单。
</div>
</div>
</div>
</body>
</html>
立即抢购(入队)
在home/平台创建Seckill控制器创建add方法
public function add()
{
#步骤1:接受数据
$uid = session('member_id');
if(!$uid) {
echo json_encode(array('state' => 0, 'msg'=>'请登录后重试...'));
die;
};
$goods_id = input('goods_id');
if(!$goods_id) {
echo json_encode(array('state' => 0, 'msg'=>'非法操作...'));
die;
}
#步骤2:连接Redis
$redis = new \Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$redis->select(0);
#步骤3:过滤(判断时间和库存)
//获取商品信息
$goodsInfo = $redis->hmget("goods_seckill_{$goods_id}", array(
'start_time', 'stop_time', 'real_num', 'seckill_num', 'price'
));
if(!$goodsInfo) {
echo json_encode(array('state' => 0, 'msg'=>'秒杀商品不存在...'));
die;
}
//判断是否开始
if ($goodsInfo['start_time'] > time()) {
echo json_encode(array('state' => 0, 'msg'=>'未开始'));
die;
}
//判断是否结束
if ($goodsInfo['stop_time'] < time()) {
echo json_encode(array('state' => 0, 'msg'=>'已结束'));
die;
}
//判断库存
if ($goodsInfo['seckill_num'] < 1) {
echo json_encode(array('state' => 0, 'msg'=>'对不起,宝贝已被抢完!'));
die;
}
#步骤4:将用户请求加入消息队列中
$len = $redis->lpush("goods_seckill_{$goods_id}_rs", $uid.'%'.$goods_id.'%'.$goodsInfo['price']);
#步骤5:判断库存(规则:队列中前n个抢购成功)
if ($len > $goodsInfo['real_num']) {
//抢购失败(队列长度 > 库存)
echo json_encode(array('state' => 0, 'msg'=>'对不起,宝贝已被抢完!'));
die;
} else {
//抢购成功,减库存(注:千万不能直接操作mysql因为有并发限制)
echo json_encode(array('state' => 1, 'msg'=>'秒杀成功'));
die;
}
}
修改秒杀列表发送异步请求
<script type="text/javascript" src="{:config('home_static')}/js/jquery-1.8.3.min.js"></script>
<script>
$(function(){
$('.buybg input').click(function(){
//禁用按钮
var thisObj = $(this);
$(this).attr('disabled', 'disabled');
$(this).css('cursor', 'default');
$(this).val('抢购中...');
var goods_id = $(this).attr('data-id');
$.post("{:url('home/seckill/add')}", {goods_id:goods_id},function(data){
if (data.state) {
alert(data.msg);
location.href = "{:url('home/order/seckill')}";
} else {
alert(data.msg)
}
//还原按钮
$(thisObj).removeAttr('disabled');
$(thisObj).css('cursor', 'pointer');
$(thisObj).val('立即抢购');
}, 'json');
});
});
</script>
使用Workerman框架(出队)
<?php
use \Workerman\Worker;
use \Workerman\Lib\Timer;
require_once __DIR__ . '/Workerman/Autoloader.php';
$task = new Worker();
// 开启多少个进程运行定时任务,注意业务是否在多进程有并发问题
$task->count = 1;
$task->onWorkerStart = function($task)
{
//每0.1秒执行一次(精度可以达到毫秒0.001)
$time_interval = 0.1;
Timer::add($time_interval, function()
{
$goods_id = 9;
#步骤1:连接Redis
$redis = new Redis;
$redis->connect('192.168.159.128', 6379);
$redis->auth('123');
$redis->select(0);
#步骤2:获取秒杀相关信息
$allowBuyNum = $redis->hget("goods_seckill_{$goods_id}", 'seckill_num'); //秒杀剩余库存
$orderInfoString = $redis->rpop("goods_seckill_{$goods_id}_rs"); //队列抢购用户信息
#步骤3:判断(有库存 && 有人抢购)
if($allowBuyNum > 0 && $orderInfoString)
{
echo "allowBuyNum:$allowBuyNum\n";
#步骤4:减库存
$redis->hincrby('goods_seckill_1', 'seckill_num', -1);
#步骤5:生成订单
$pdo = new \PDO('mysql:dbname=seckill', 'root', 'root');
$userOrderInfo = explode('%', $orderInfoString); //$uid.'%'.$goods_id.'%'.$price
$order_id = date('Ymd').time().uniqid();
$total_price = $userOrderInfo[2];
$member_id = $userOrderInfo[0];
$goods_id = $userOrderInfo[1];
$create_time = time();
$update_time = time();
#主表(sh_order)
$sql = "insert into sh_order (is_seckill, order_id, total_price, member_id, create_time, update_time)
value
(1, '{$order_id}', $total_price, $member_id, $create_time, $update_time)";
$pdo->exec($sql);
#从表(sh_order_goods)
$sql = "insert into sh_order_goods (order_id, goods_id, goods_number, goods_price) value('{$order_id}', $goods_id, 1, $total_price)";
$pdo->exec($sql);
echo "over...\n";
}
});
};
// 运行worker
Worker::runAll();
网友评论