简述tp框架结合Redis高并发商品秒杀解决方案

流氓凡 技术分享 2022-03-11 4.53 K 0

前言

假设在一个并发量较高的场景,数据库中 num 的值为 1 时,可能同时会有多个进程读取到 num 为 1,程序判断符合条件,抢购成功,num 减一。这样会导致商品超发的情况,本来只有 10 件可以抢购的商品,可能会有超过 10 个人抢到,此时 num 在抢购完成之后为负值。

解决该问题的方案由很多,可以简单分为基于 mysql 和 redis 的解决方案,redis 的性能要由于 mysql,因此可以承载更高的并发量,不过下面介绍的方案都是基于单台 mysql 和 redis 的,更高的并发量需要分布式的解决方案,本文没有涉及。


基于 watch 的乐观锁方案

watch 用于监视一个 (或多个) key ,如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。这种方案跟 mysql 中的乐观锁方案类似,具体表现也是一样的。

首先我们搞个示例数据表:

CREATE TABLE `am_goods` (
  `goods_id` int(11) NOT NULL AUTO_INCREMENT,
  `num` int(11) NOT NULL DEFAULT '0',
  `name` varchar(100) NOT NULL DEFAULT '',
  `create_time` int(11) NOT NULL DEFAULT '0',
  `update_time` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `am_goods` (`goods_id`, `num`, `name`, `create_time`, `update_time`) VALUES
(10001,	69,	'测试商品',	1646969321,	1646969321);

我这里使用的是tp5.0框架,redis对象示例需要调整下才能返回,修改/Applications/MAMP/htdocs/tp5.test.noteo.cn/thinkphp/library/think/cache/driver/Redis.php 添加下面方法:

/*
     * 返回原生的redis对象
     * @return object
    */
    public function getHandler()
    {
        return $this->handler;
    }

核心代码示例:

        $redis = new redis();
        $redis = $redis->getHandler();
        $redis->connect('127.0.0.1',6379);
        $redis->select(6);
        //第一次取库存,先用保存到缓存中(后续更新库存需要把这个缓存移除)
        $goods_id = 10001;
        $stock_key = 'goods_id_' . $goods_id;
        //先把库存取出来
        $stock = $redis->get($stock_key);
        //监控key
        $redis->watch($stock_key);
        //开启事务
        $redis->multi();
        if ($stock === false) {
            // 读数据库库存
            $stock = db('goods')->where('goods_id', $goods_id)->value('num');
            $redis->setnx($stock_key, $stock);
        }
        if ($stock > 0) {
            $redis->decr($stock_key);
            // 这里可以sleep 1秒 来观察效果
            $res = $redis->exec();
            if ($res !== false) {
                db('goods')->where('goods_id', $goods_id)->setDec('num');
                // todo: 接入队列后续处理相关用户订单
                return "成功了";
            }
            return "失败了";
        }
        return "无库存";

其他方案

仅仅对于redis还有一些其他解决方案,比如说list,是指在抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。

但是这样会造成大量的内存负载不推荐使用。


基于 decr 返回值的方案

如果我们将剩余量 num 设置为一个键值类型,每次先 get 之后判断,然后再 decr 是不能解决超发问题的。但是 redis 中的 decr 操作会返回执行后的结果,可以解决超发问题。我们首先 get 到 num 的值进行第一步判断,避免每次都去更新 num 的值,然后再对 num 执行 decr 操作,并判断 decr 的返回值,如果返回值不小于 0,这说明 decr 之前是大于 0 的,用户抢购成功。


甚至我们还可以使用文件锁来解决,本博中也提供了文件锁示例,搜索下即可。


评论