NoSQL(Redis)秒杀

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


    
        {foreach $seckills as $seckill}
        
        {/foreach}
    
商品ID 商品名称 商品价格 商品数量 开始时间 结束时间 距离结束
{$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

距离倒计时(修改控制器 ) 增加字段

$seckill->time = $temp['stop_time']

距离倒计时(修改视图)




    
    Document
    
    



    
        {foreach $seckills as $seckill}
        
        {/foreach}
    
商品ID 商品名称 商品价格 商品数量 开始时间 结束时间 距离结束
{$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

完成前台秒杀功能

修改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}
    价格:{$seckill.price}
    库存:{$seckill.seckill_num}
    {/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();
    

    你可能感兴趣的:(NoSQL(Redis)秒杀)