php+redis+swoole协程打造高并发秒杀系统

上一篇文章我们介绍了swoole搭建HTTP服务端,
这篇文章结合上篇文章中的内容,结合Redis列表打造高并发的秒杀系统,
秒杀系统的重点主要在于: 保证大量用户访问的同时,系统正常顺畅运行,商品库存不超卖

再开始之前,我们先将所需的Redisswoole安装完成,介绍秒杀具体操作流程:

1. 将秒杀商品的基本信息,使用hash存入Redis
2. 用户点击抢购时,将用户ID存入Redis列表中,同时根据库存,限制列表长度
3. 使用swoole新建协程,根据列表先进先出,异步实现商品抢购
4. 使用定时器,定时向Redis中查询抢购结果,设置超时时间,返回抢购结果

这里redis我们使用了两个hash、一个list来存储相关信息:

// 当前商品的库存等信息(hash,例:swoole:goods_10)
protected $goods_key;
// 当前商品队列的用户情况(list,例:swoole:goods_10_user)
protected $user_queue_key;
// 抢购成功的用户订单(hash,例:swoole:goods_10_order)
protected $order_queue_key;

一、存入抢购商品信息

public function goods()
{
    $this->setGoods([
        'goods_id'      => 10, // 商品ID
        // 'price'         => $data['price'], // 价格
        'prom_id'       => 10, // 活动ID
        'buy_limit'     => 1, // 每人限购
        'store_count'   => 99, // 库存
        'sell_count'    => 0, // 销量
        'start_time'    => time(), // 开始时间
        'end_time'      => strtotime("+1 day"), // 截止时间
        'status'        => 0, //用来判断协程运行状态
    ]);
    return $this->getGoods(10);
}

开启swoole服务,访问 http://127.0.0.1:9501/Flashsale/goods?id=1,保存商品信息

QQ截图20240901162222.png

二、完整抢购示例代码(feng/Flashsale.php)

<?php
/**
 * @Author: [FENG] <1161634940@qq.com>
 * @Date:   2024-08-20 15:09:00
 * @Last Modified by:   [FENG] <1161634940@qq.com>
 * @Last Modified time: 2024-09-01 16:10:10
 */
namespace feng;

/**
 * 秒杀系统
 */
class Flashsale
{
    // 键名
    protected $key;
    // redis实例化
    protected $redis;

    protected $server;
    protected $frame;
    // swoole Common类实例
    protected $swooleCommon;

    // 当前商品的库存等信息
    protected $goods_key;
    // 当前商品队列的用户情况
    protected $user_queue_key;
    // 抢购成功的用户
    protected $order_queue_key;

    protected $field = ['prom_id', 'price', 'buy_limit', 'store_count', 'sell_count', 'start_time', 'end_time'];

    public function __construct($base = [])
    {
        if ($base) {
            [$this->server, $this->frame, $this->swooleCommon] = $base;
        }

        $config = [
            "hostname"  => '127.0.0.1', //Redis主机IP地址
            "hostport"  => 6379, //Redis端口
            "password"  => '', //Redis密码
            "select"    => 6, //Redis数据库索引
            "timeout"   => 0, //Redis连接超时时间
        ];
        $this->redis = $this->redisInit($config); // 实例化redis
        $this->key = 'swoole:'; // 个人习惯redis加上前缀
    }

    /**
     * [goods 存入商品信息]
     */
    public function goods()
    {
        $this->setGoods([
            'goods_id'      => 10, // 商品ID
            // 'price'         => $data['price'], // 价格
            'prom_id'       => 10, // 活动ID
            'buy_limit'     => 1, // 每人限购
            'store_count'   => 99, // 库存
            'sell_count'    => 0, // 销量
            'start_time'    => time(), // 开始时间
            'end_time'      => strtotime("+1 day"), // 截止时间
            'status'        => 0, //用来判断协程运行状态
        ]);
        return $this->getGoods(10);
    }

    /**
     * [FlashSale 抢购详情筛选]
     */
    public function flashSale($data = [])
    {
        $data = $data ?: input('');
        if (empty($data['id']))
            return result(0, '请选择抢购的商品');
        if (empty($data['user_id']))
            return result(0, '请登录后进行操作');

        $redis = $this->redis;
        $goods = $this->getGoods($data['id']);
        if (!$goods || time() > $goods['end_time'])
            return result(0, '当前商品抢购已结束。');
        if (time() < $goods['start_time'])
            return result(0, '抢购尚未开始,请耐心等待');

        $goods_id = $data['id'];
        $user_id = $data['user_id'];

        $this->goods_key = $this->key . "goods_".$goods_id;
        $this->user_queue_key = $this->key . "goods_".$goods_id."_user";
        $this->order_queue_key = $this->key . "goods_".$goods_id."_order";

        if ($goods['store_count'] == $goods['sell_count'] || $redis->llen($this->user_queue_key) > $goods['store_count'] * 1.5 ) {
            if (time() - $goods['start_time'] > 600) {
                // 查询数据库,获取未支付订单数量
                $count = Db::name('order')->where(['order_prom_id'=>$goods['prom_id'], 'order_prom_type'=>1, 'pay_status'=>0])->count();
                if ($count > 0) {
                    return result(0, '当前抢购存在未支付的订单,请稍后再次尝试');
                }
            }
            return result(0, '当前商品已被抢完。');
        }

        if ($redis->exists($this->order_queue_key) && $order_id = $redis->hget($this->order_queue_key, $user_id)) {
            return result(0, '你已参与过活动,请勿重复添加');
        }

        if (!$this->hasExist($this->user_queue_key, $user_id)) {
            $redis->rpush($this->user_queue_key, $user_id);
        }

        $ststus = $redis->rawCommand('hmget', $this->goods_key, 'status');

        if ($ststus != 1) {
            // 消费者协程,单独执行抢购
            go(function () use ($redis, $data, $goods) {
                $redis->hset($this->goods_key, 'status', 1);

                while($user_id = $redis->lpop($this->user_queue_key)) {
                    try {
                        if (!$user_id || $redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count')) {
                            $redis->hset($this->goods_key, 'status', 0);
                            die('抢购协程执行结束');
                        }
                        $order_id = $this->add_order($goods, $data['user_id']); // 商品加入数据库(添加订单十分钟失效)
                        if ($order_id) {
                            $redis->hset($this->order_queue_key, $user_id, $order_id);
                            $redis->hincrby($this->goods_key, 'sell_count', 1);
                        }
                        if ($redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count')) {
                            $redis->del($this->user_queue_key);
                        }
                        if ($redis->llen($this->user_queue_key) == 0) {
                            $redis->hset($this->goods_key, 'status', 0);
                            die('抢购协程执行结束');
                        }
                    } catch (\Swoole\ExitException $e) {
                        // global $exit_status;
                        // $exit_status = $e->getStatus();
                    }
                }
            });
        }

        $data['count'] = 0;
        $timer_id = $this->server->tick(200, function($timer_id) use ($redis, &$data, $user_id){
            $json = '';
            if (!$json && $order_id = $redis->hget($this->order_queue_key, $user_id))
                $json = msg(1, '抢购成功.', $order_id);

            if (!$json && $redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count'))
                $json = msg(0, '当前商品已被抢光。');

            if ($data['count'] > 100)
                $json = msg(0, '当前活动过于火爆,请稍后尝试');

            if ($json) {
                $this->server->clearTimer($timer_id);
                $this->frame->end($json);
            }
            $data['count']++;
        });
    }

    // 生成订单(存入mysql数据库)
    public function add_order($goods, $user_id)
    {
        $order_id = rand(000, 999); // 订单设置15分钟支付超时,同时超时取消订单,释放库存
        return $order_id;
    }

    // 初始化redis
    public function redisInit($config)
    {
        if (!extension_loaded('redis')) {
            throw new \Exception('redis扩展未安装');
        }

        $func        = empty($config['persistent']) ? 'connect' : 'pconnect';
        $redis = new \Redis;
        if (isset($config['socket_type']) && $config['socket_type'] === 'unix') {
            $success = $redis->$func($config['socket']);
        } else {
            $success = $redis->$func($config['hostname'], $config['hostport'], $config['timeout']);
        }
        if (isset($config['password'])) {
            $redis->auth($config['password']);
        }

        if (isset($config['select']) && !empty($config['select'])) {
            $redis->select(intval($config['select']));
        }

        return $redis;
    }

    public function setGoods($goods = [])
    {
        if (is_array($goods) && isset($goods['goods_id'])) {
            foreach ($this->field as $v) {
                if (isset($goods[$v])) {
                    $this->redis->hset($this->key . "goods_".$goods['goods_id'], $v, $goods[$v]);
                }
            }
        } else {
            $this->redis->del($this->key . "goods_".$goods);
            // $this->redis->hdel($goods, 'buy_limit', 'store_count', 'start_time', 'end_time');
        }
    }

    public function getGoods($goods_id, $key = '')
    {
        if (in_array($key, $this->field)) {
            return $this->redis->hget($this->key . "goods_".$goods_id, $key);
        } else {
            return $this->redis->hgetall($this->key . "goods_".$goods_id);
        }
    }

    public function hasExist($key, $element = '')
    {
        $redis = $this->redis;
        if (!$element) {
            // 使用lrange获取整个list
            $list = $redis->lrange($key, 0, -1);
            return $list;
        }

        // Lua脚本检查元素是否在list中
        $script = <<<LUA
        local key = KEYS[1]
        local value = ARGV[1]
        local element = redis.call('lrange', key, 0, -1)
        for i, v in ipairs(element) do
            if v == value then
                return 1
            end
        end
        return 0
LUA;

        $result = $redis->rawCommand("EVAL", $script, 1, $key, $element);
        return $result == 1 ? true : false;
    }

}

/**
 * [result 返回状态数组]
 * @param  [type] $code [错误码]
 * @param  string $data [具体数据]
 * @return [type]       [description]
 */
function result($code, $msg=true, $data=false)
{
    if (is_array($code) || $code === true || $msg === true) {
        $result = ['code'=>1];
        $result['err'] = is_string($msg) ? $msg : '操作成功';
        $result['data'] = $code;
    } else {
        $result = ['code'=>$code, 'err'=>$msg, 'data'=>$data];
    }
    return $result;
}

访问 url http://127.0.0.1:9501/Flashsale/flashSale?id=10&user_id=1,执行抢购(这里user_id建议使用token获取)

QQ截图20240901162325.png

冯奎博客
请先登录后发表评论
  • latest comments
  • 总共0条评论