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输入下述命令
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输入手册中的定时器代码
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输入下述命令
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输入下述命令
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('');
}
创建视图文件
商品秒杀配置
数据处理:修改商品状态为秒杀 将秒杀商品数据保存的redis中
redis键规则:
hmset goods_seckill_1 start_time 0 stop_time 0 price 30 real_num 3 seckill_num 3
秒杀商品配置
修改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目录好
商品秒杀页配置引入
商品秒杀配置
修改控制器方法格式化日期
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
]);
}
创建视图并循环显示数据
Document
商品ID
商品名称
商品价格
商品数量
开始时间
结束时间
距离结束
{foreach $seckills as $seckill}
{$seckill.goods_id}
{$seckill.goods_name}
{$seckill.price}
{$seckill.real_num}
{:date('Y-m-d H:i:s', $seckill.start_time)}
{:date('Y-m-d H:i:s', $seckill.stop_time)}
0
{/foreach}
距离倒计时(修改控制器 ) 增加字段
$seckill->time = $temp['stop_time']
距离倒计时(修改视图)
Document
商品ID
商品名称
商品价格
商品数量
开始时间
结束时间
距离结束
{foreach $seckills as $seckill}
{$seckill.goods_id}
{$seckill.goods_name}
{$seckill.price}
{$seckill.real_num}
{:date('Y-m-d H:i:s', $seckill.start_time)}
{:date('Y-m-d H:i:s', $seckill.stop_time)}
0
{/foreach}
完成前台秒杀功能
修改home/index/index
商品秒杀
在后台创建seckill控制器index方法
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
]);
}
}
创建视图并循环显示数据
一元秒杀!还包邮!
{foreach $seckills as $seckill}
{/foreach}
1元秒杀细则:
1.参与秒杀前,请详细阅读秒杀规则,凡参与1元秒杀活动的用户,均视为同意秒杀规则。
2.秒杀商品将于2014年7月7日08:00:00上线-2014年7月11日23:59:59结束,当天商品售罄时当天秒杀结束,活动期间每一个云中央注册会员每期仅限秒杀一个商品,秒杀多件成功者,并通过收货人及联系方式可判定为同一人的,则取消全部订单。
3.秒杀成功以支付成功为准,早秒早得;秒杀下单后30分钟内未付款者自动取消订单,请特别注意。
4.请确保秒杀填写的收货人信息真实有效,因联系方式填写错误导致未收到礼品的,由用户自行承担损失。
5.对于任何通过不正当手段参与秒杀者,不正当手段包括但不限于使用秒杀器或类似作弊软件,云中央网站有权依据自身技术判断,并在不事先通知的情况下取消其秒杀资格或者取消订单。
立即抢购(入队)
在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;
}
}
修改秒杀列表发送异步请求
使用Workerman框架(出队)
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();