Thinkphp6使用redis实现商品秒杀

使用redis lpop

redis的list比decr有更大的优势 可自行百度 本次使用list

压测使用的的apache


ab -n 1000 -c 20 -T application/json http://test1.com/api/shop/skillsJobs?goods_id=4

-n 1000:总共发送 1000 个请求。
-c 20:并发数为 20-k:保持长连接。
-T "application/json":设置请求头为 application/json。
-p 指定 POST 数据文件。

数据库设计

CREATE TABLE `shop_goods` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `goods_cate_id` int(10) unsigned NOT NULL COMMENT '商品分类',
  `name` varchar(255) NOT NULL COMMENT '商品名称',
  `code` varchar(255) DEFAULT NULL COMMENT '商品编码',
  `img_thumb` int(11) DEFAULT NULL COMMENT '商品图片URL',
  `des` text COMMENT '商品描述',
  `all_num` int(10) unsigned DEFAULT '0' COMMENT '商品总库存',
  `status` tinyint(3) unsigned DEFAULT '1' COMMENT '状态:1可用,0不可用',
  `is_delete` tinyint(3) unsigned DEFAULT '0' COMMENT '是否删除:1已删除,0未删除',
  `sort` int(10) unsigned DEFAULT '0' COMMENT '排序字段',
  `amount` decimal(10,2) DEFAULT '1.00' COMMENT '商品价格,默认给1',
  `add_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `start_time` int(11) DEFAULT '0',
  `end_time` int(11) DEFAULT '0',
  `skill_state` tinyint(1) DEFAULT '0' COMMENT '是否开启秒杀0不开  1开启',
  PRIMARY KEY (`id`),
  KEY `goods_cate_id` (`goods_cate_id`),
  CONSTRAINT `shop_goods_ibfk_1` FOREIGN KEY (`goods_cate_id`) REFERENCES `shop_goods_cate` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表';

CREATE TABLE `shop_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `order_sn` varchar(50) NOT NULL COMMENT '订单号',
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `goods_id` int(11) NOT NULL COMMENT '商品ID',
  `price` decimal(10,2) NOT NULL COMMENT '价格',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态',
  `addtime` datetime NOT NULL COMMENT '添加时间',
  `is_delete` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `order_sn` (`order_sn`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';

--- jobs队列使用的方案
CREATE TABLE `jobs` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `queue` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `payload` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `attempts` int(10) unsigned NOT NULL,
  `reserve_time` int(10) unsigned DEFAULT NULL,
  `available_time` int(10) unsigned NOT NULL,
  `create_time` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  KEY `queue` (`queue`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=51698 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

这一步不管是直接操作库还是使用jobs队列都需要提前写入到redis

写入redis库存数量

	public function initSeckillStock($param)
    {
        $goodsId = $param['goods_id'];
        $redisKey = 'goods_num_' . $goodsId;
        //清楚redisKey
        /**
         * 1.写入秒杀商品不能再次编辑
         * 2.如需要编辑则同步移除redisKey
         */
        #Cache::store('redis')->delete($redisKey);
        $info = $this->dao->search([
                'status' => 1,
                'skill_state' => 1,
                'all_num' => ['>', 0],
                'start_time' => ['<=', time()],
                'end_time' => ['>', time()]
            ])
            ->field('id,name,status,amount,all_num,end_time,start_time,skill_state')
            ->where('id', $goodsId)
            ->find()->toArray();
        if (empty($info)) {
            throw new ValidateException('商品不存在或不符合秒杀条件');
        }
        $len = Cache::store('redis')->lLen($redisKey);
        if (!$len || $len <= 0) {
            for ($i = 1; $i <= $info['all_num']; $i++) {
                Cache::store('redis')->lpush($redisKey, 1);
            }
        }

        $expTime = $info['end_time'] - time();
        #写入redisKey过期时间 以及商品信息检测该商品是否还需要秒杀
        Cache::store('redis')->set('goods_info' . $goodsId, json_encode($info), $expTime);
        return $info;
    }

第一种方案直接秒杀操作数据库

 public function skill($param)
    {
		$goodsId = $param['goods_id'];
        $goodsInfoKey = 'goods_info' . $goodsId;
        $redisKey = 'goods_num_' . $goodsId;
        $goodsUserKey = 'seckill_users:' . $goodsId;
        $goodsInfo = Cache::store('redis')->get($goodsInfoKey);
        $goodsInfo = $goodsInfo ? json_decode($goodsInfo, true) : [];
        if (!$goodsInfo) {
            throw new ValidateException('秒杀商品不存在');
        }
        #验证秒杀开启时间+结束时间
        $startTime = strtotime($goodsInfo['start_time']);
        $endTime = strtotime($goodsInfo['end_time']);
        if (time() < $startTime || time() > $endTime) {
            throw new ValidateException('活动未开始或已结束');
        }

        $len = Cache::store('redis')->lLen($redisKey);
        if (!$len || $len <= 0) {
            throw new ValidateException('商品库存不足');
        }
        $uid = rand(1, 50);    //随机生成用户id
        $dataLog['uid'] = "用户" . $uid;
        Db::startTrans();
        try {
            // 1. 防用户重复秒杀检查(使用Redis集合)
            if (Cache::store('redis')->sIsMember($goodsUserKey, $uid)) {
                throw new ValidateException('请勿重复参与');
            }
            // 自动减少一个库存
            $result = $this->dao->search([])->where('id', $goodsId)->dec('all_num')->update();;
            if ($result) {
                $lenLpop = Cache::store('redis')->lpop($redisKey);
                if (!$lenLpop) {
                    throw new ValidateException('商品已售罄');
                }
                Cache::store('redis')->sAdd($goodsUserKey, $uid);
                $lens = Cache::store('redis')->lLen($redisKey);
                $insert_data = [
                    'order_sn' => $this->buildOrderNo(),   //生成订单,
                    'user_id' => $uid,
                    'goods_id' => $goodsId,
                    'price' => $goodsInfo['amount'],
                    'status' => 1,
                    'addtime' => date('Y-m-d H:i:s')
                ];
                $shopOrderRepository = app()->make(ShopOrderRepository::class);
                // 订单入库
                $res = $shopOrderRepository->create($insert_data);
                if ($res) {
                    $msg = "第" . $lens . "件秒杀成功";
                    $dataLog['信息'] = $msg;
                    echo $msg;
                    $this->writeLog($dataLog, 1);
                } else {
                    $msg = "第" . $lens  . "件秒杀失败";
                    $dataLog['信息'] = $msg;
                    echo $msg;
                    $this->writeLog($dataLog, 0);
                }
                Db::commit();
                return [];
            } else {
                Db::rollback();
                throw new ValidateException('购买失败');
            }
        } catch (\Exception $e) {
            Db::rollback();
            $dataLog['信息'] = $e->getMessage();
            $this->writeLog($dataLog);
            throw new ValidateException('购买失败' . $e->getMessage());
        }
    }
    /**
     * 订单号
     * @return string
     */
    public function buildOrderNo()
    {
        return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
    }

    /**
     * 生成日志
     */
    public function writeLog($msg, $status = 0)
    {
        $data['count'] = 1;
        $data['status'] = 0;
        $data['add_time'] = date('Y-m-d H:i:s');
        $data['msg'] = $msg;
        Log::info(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

第二种使用queue队列

use think\facade\Queue;

 public function skillJobs($param)
    {
        $goodsId = $param['goods_id'];
        $goodsInfoKey = 'goods_info' . $goodsId;
        $redisKey = 'goods_num_' . $goodsId;
        $goodsUserKey = 'seckill_users:' . $goodsId;
        $goodsInfo = Cache::store('redis')->get($goodsInfoKey);
        $goodsInfo = $goodsInfo ? json_decode($goodsInfo, true) : [];
        if (!$goodsInfo) {
            throw new ValidateException('秒杀商品不存在');
        }
        $startTime = strtotime($goodsInfo['start_time']);
        $endTime = strtotime($goodsInfo['end_time']);
        if (time() < $startTime || time() > $endTime) {
            throw new ValidateException('活动未开始或已结束');
        }

        $len = Cache::store('redis')->lLen($redisKey);
        if (!$len || $len <= 0) {
            throw new ValidateException('商品库存不足');
        }
        $uid = rand(0, 50);    //随机生成用户id
        $dataLog['uid'] = "用户" . $uid;
        Db::startTrans();
        try {
            // 1. 防重复秒杀检查(使用Redis集合)
            if (Cache::store('redis')->sIsMember($goodsUserKey, $uid)) {
                throw new ValidateException('请勿重复参与');
            }
            $lenLpop = Cache::store('redis')->lpop($redisKey);
            if (!$lenLpop) {
                throw new ValidateException('商品已售罄');
            }
            Cache::store('redis')->sAdd($goodsUserKey, $uid);
            $lens = Cache::store('redis')->lLen($redisKey);
            $jobQueueName  	  = "updateData";
            // 4. 推送写入订单任务到队列
            $isPushed = Queue::push('app\jobs\ShopOrderJob', [
                'user_id' => $uid,
                'goods_id' => $goodsId,
                'price' => $goodsInfo['amount'],
                'len' => $lens 
            ], $jobQueueName);
            if( $isPushed !== false ){
                echo date('Y-m-d H:i:s') . " a new Hello Job is Pushed to the MQ"."<br>";
            }else{
                echo 'Oops, something went wrong.';
            }
            Db::commit();
            return json()->data(['code' => 0, 'msg' => '购买成功']);
        } catch (\Exception $e) {
            Db::rollback();
            $dataLog['信息'] = $e->getMessage();
            $this->writeLog($dataLog);
            throw new ValidateException('购买失败' . $e->getMessage());
        }
    }

队列job文件

<?php

namespace app\jobs;

use app\common\repositories\shop\ShopGoodsRepository;
use app\common\repositories\shop\ShopOrderRepository;
use think\facade\Log;
use think\queue\Job;

class ShopOrderJob
{
    public function fire(Job $job, $data)
    {
        try {
            $this->updateData($data);
        } catch (\Exception $e) {
            dump($e);
            exception_log('任务处理失败', $e);
        }
        $job->delete();

    }

    public function updateData($data)
    {

        try {
            $shopOrderRepository = app()->make(ShopOrderRepository::class);
            $insert_data = [
                'order_sn' => $this->buildOrderNo(),   // 生成订单
                'user_id'  => $data['user_id'],
                'goods_id' => $data['goods_id'],
                'price'    => $data['price'],
                'status'   => 1,
                'addtime'  => date('Y-m-d H:i:s')
            ];
             $dataLog['uid'] = "用户" . $data['user_id'];
            $res = $shopOrderRepository->create($insert_data);
            $shopGoodsRepository = app()->make(ShopGoodsRepository::class);
            $shopGoodsRepository->search(['id' => $data['goods_id']])->dec('all_num')->update();
            if ($res) {
                $msg = "第" . $data['len'] . "件秒杀成功";
               
                $dataLog['信息'] = $msg;
                echo $msg;
                $this->writeLog($dataLog, 1);
            } else {
                $msg = "第" . $data['len'] . "件秒杀失败";
                $dataLog['信息'] = $msg;
                echo $msg;
                $this->writeLog($dataLog, 0);
            }
            return true;
        } catch (\Exception $e) {
            $dataLog['uid'] = "用户" . $data['user_id'];
            $dataLog['信息'] = $e->getMessage();
            $this->writeLog($dataLog);
            return false;
        }
    }

    /**
     * 订单号
     * @return string
     */
    public function buildOrderNo()
    {
        return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
    }

    /**
     * 生成日志
     */
    public function writeLog($msg, $status = 0)
    {
        $data['count']  = 1;
        $data['status'] = $status;
        $data['add_time'] = date('Y-m-d H:i:s');
        $data['msg'] = $msg;
        Log::info(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    public function failed($data)
    {
        // ...任务达到最大重试次数后,失败了
    }

}

监听queue

gitee的链接自行使用listen和work区别

 php think queue:work --queue updateData

在这里插入图片描述
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9d66f57ab5c94a8090d8a1e5194b1214.png
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值