简述tp框架结合Redis高并发商品秒杀解决方案
前言
假设在一个并发量较高的场景,数据库中 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 的,用户抢购成功。
甚至我们还可以使用文件锁来解决,本博中也提供了文件锁示例,搜索下即可。
评论