高并发下防止库存超卖解决方案

一、概述

目前网上关于防止库存超卖,我没找到可以支持一次购买多件的,都是基于一次只能购买一件做的秒杀方案,但是实际场景中,一般秒杀活动都是支持1~5件的,因此为了补缺,写了此文,方便自己之后使用。

 
二、建表


1、商品表

CREATE TABLE `product_test` (`product_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '商品ID',`stock` int(11) unsigned DEFAULT NULL COMMENT '商品库存',PRIMARY KEY (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';

2、订单记录表

CREATE TABLE `order_test` (`order_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单ID',`product_id` int(10) unsigned NOT NULL COMMENT '商品ID',`sale` int(11) DEFAULT '0' COMMENT '下单数量',PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单日志表';

3、向商品表内插入一条记录

INSERT INTO product_test (`stock`) VALUES (1300);


 
三、不同方案下测试报告

方案一:只将MySQL 库存字段设置了无符号

该方案存在超卖情况。测试总请求数20000,并发数8000的情况下,反应较慢。该方案在高并发下不可行,不是高并发情况下也有风险。


 
方案二:使用MySQL排它锁(for update)

该方案无超卖情况,但是响应时间过长。我使用20000请求,8000并发的测试情况下,时间均在11s ~ 13s 之间响应速度感人,不推荐高并发下采用该方案。


 
方案三:使用Redis队列【推荐方案】

该方案无超卖情况,相应速度较MySQL排它锁方案响应速度提高很多使用20000总请求,8000并发,每个请求平均响应时间5.38秒
使用30000总请求,1000并发,每个请求平均响应时间3.77秒
使用10000总请求,1000并发,每个请求平均响应时间1.23秒
使用5000总请求,1000并发,每个请求平均响应时间0.61秒需要注意,采用该方案,Redis中的商品库存数据一定要提前生成,而不是等查询时生成,应该增加商品数据时,就实时添加库存数据到Redis中,之后所有操作都从Redis操作(包括增删改查),之后持久化同步到数据库,可以采用异步消息队列方式。如果是旧系统,则应该写个脚本,先把数据库上只读锁,然后将商品库存预热到Redis中,再解开MySQL的只读锁,之后所有库存操作都在Redis中进行


 
方案四:使用Redis事务监听【待补充】

 

 

四、代码部分

方案一、只将库存字段设置为无符号

<?php
declare(strict_types = 1);class OperateStock
{protected $pdo = null;CONST REDUCE_STOCK = 1;// 减少库存操作CONST INCREASE_STOCK = 2;// 增加库存操作/*** 下订单扣减库存** @param int $productId* @param int $num*/public function placeOrder(int $productId, int $num){$stock = $this->getProductStock($productId);if ($num > $stock) {return $this->response(0, '超出库存,无法下单');}// 执行扣减库存操作$res = $this->changeStock($productId, $num);// 记录日志$this->recordOrderLog($productId, $num);return $res;}/*** 修改商品库存** @param int $productId* @param int $num* @param int $action* @return string*/private function changeStock(int $productId, int $num, int $action = self::REDUCE_STOCK){$operateAction = $action == self::REDUCE_STOCK ? '-' : '+';try {$sql = 'update product_test set stock = stock '.$operateAction.' '.$num. " where product_id = $productId";$this->getMySQL()->query($sql);} catch (\Exception $e) {return $this->response(0, $e->getMessage());}return $this->response(1, 'success');}/*** 记录销售日志** @param int $productId* @param int $num*/private function recordOrderLog(int $productId, int $num){$sql = "insert into order_test (product_id,sale) values ($productId,$num)";$this->getMySQL()->query($sql);}/*** 获取MySQL连接** @return PDO*/private function getMySQL(){if (false == $this->pdo) {$dsn = 'mysql:host=127.0.0.1;dbname=test';$this->pdo = new \PDO($dsn, 'root', '123456');}return $this->pdo;}/*** 获取商品库存数** @param $productId* @return mixed*/private function getProductStock($productId){// 查询库存$sql = "select stock from product_test where product_id = $productId limit 1";$stock = $this->getMySQL()->query($sql)->fetch(2);return $stock['stock'];}/*** 统一响应** @param int $statusCode* @param string $msg* @param array $data* @return string*/private function response(int $statusCode, string $msg, array $data = []){$data = ['status' => $statusCode,'msg'    => $msg,'data'   => $data];return json_encode($data);}
}
// 生成随机的商品下单数,使用ab压测工具测试并发下是否超卖 ab -n 20000 -c 8000 http://test.cn/
$num = rand(1, 300);
$object = new OperateStock();
print_r($object->placeOrder(1, $num));

 
2、使用 MySQL 排它锁(FOR UPDATE )方案

在事务中执行,并且在首次查商品剩余库存时,就将排它锁加上,注意,是查询时就加上,而不是操作时再加

<?php
declare(strict_types = 1);class OperateStock
{protected $pdo = null;CONST REDUCE_STOCK = 1;// 减少库存操作CONST INCREASE_STOCK = 2;// 增加库存操作/*** 下订单扣减库存** @param int $productId* @param int $num*/public function placeOrder(int $productId, int $num){try {// 开启事务$this->getMySQL()->beginTransaction();$stock = $this->getProductStock($productId);if ($num > $stock) {return $this->response(0, '超出库存,无法下单');}// 执行扣减库存操作$res = $this->changeStock($productId, $num);// 记录日志$this->recordOrderLog($productId, $num);$this->getMySQL()->commit();} catch (\Exception $e) {$this->getMySQL()->rollBack();$this->response(0, $e->getMessage());}return $res;}private function changeStock(int $productId, int $num, int $action = self::REDUCE_STOCK){$operateAction = $action == self::REDUCE_STOCK ? '-' : '+';try {$sql = 'update product_test set stock = stock '.$operateAction.' '.$num. " where product_id = $productId";$this->getMySQL()->query($sql);} catch (\Exception $e) {return $this->response(0, $e->getMessage());}return $this->response(1, 'success');}/*** 记录销售日志** @param int $productId* @param int $num*/private function recordOrderLog(int $productId, int $num){$sql = "insert into order_test (product_id,sale) values ($productId,$num)";$this->getMySQL()->query($sql);}/*** 获取MySQL连接** @return PDO*/private function getMySQL(){if (false == $this->pdo) {$dsn = 'mysql:host=127.0.0.1;dbname=test';$this->pdo = new \PDO($dsn, 'root', '123456');}return $this->pdo;}/*** 获取商品库存数** @param $productId* @return mixed*/private function getProductStock($productId){// 查询库存$sql = "select stock from product_test where product_id = $productId limit 1 for update";$stock = $this->getMySQL()->query($sql)->fetch(2);return $stock['stock'];}/*** 统一响应** @param int $statusCode* @param string $msg* @param array $data* @return string*/private function response(int $statusCode, string $msg, array $data = []){$data = ['status' => $statusCode,'msg'    => $msg,'data'   => $data];return json_encode($data);}/*** 获取日志表中销量总量** @author cyf*/public function getSaleSum(int $productId){$sql = 'select sum(sale) from order_test where product_id = '.$productId;$data = $this->getMySQL()->query($sql)->fetch(2);return $this->response(1, 'success', [$data]);}}
// 生成随机的商品下单数,使用ab压测工具测试并发下是否超卖 ab -n 10000 -c 5000 http://test.cn/
$num = rand(1, 300);
$object = new OperateStock();
print_r($object->placeOrder(1, $num));
//print_r($object->getSaleSum(1));

 
3、使用 Redis 队列方案【推荐】

<?php
declare(strict_types = 1);class OperateStock
{protected $pdo = null;protected $redis = null;protected $stockKeyPre = 'product_stock_';// 商品库存redis key前缀CONST REDUCE_STOCK = 1;// 减少库存操作CONST INCREASE_STOCK = 2;// 增加库存操作/*** 下订单扣减库存** @param int $productId* @param int $num* @return string* @throws Exception*/public function placeOrder(int $productId, int $num){try {$this->reduceStock($productId, $num);// 推送消息队列,对数据库中库存数据进行异步扣减// 记录订单销售日志$this->recordOrderLog($productId, $num);return $this->response(1, '下单成功');} catch (\Exception $e) {return $this->response(0, $e->getMessage());}}/*** 扣减库存** @param int $productId* @param int $num* @return bool* @throws Exception*/private function reduceStock(int $productId, int $num){$redis = $this->getRedis();$key = $this->stockKeyPre.$productId;$valueArray = [];try {for ($i = 0; $i < $num; $i++) {$res = $redis->rPop($key);if (false == $res) {throw new \Exception('库存不够啦');}$valueArray[] = $res;}return true;} catch (\Exception $e) {// 手动对已经下单的数据进行回滚,并抛出异常给上游调用方foreach ($valueArray as $v) {$redis->lPush($key, $v);}throw new \Exception('库存不够啦');}}/*** 增删改商品时,重置Redis内的该商品的库存【测试方法】** @author cyf*/public function resetStockToRedis(int $productId, int $num){$redis = $this->getRedis();$key = $this->stockKeyPre.$productId;for($i = 1; $i <= $num; $i++) {$redis->lpush($key, $i);}return $this->response(1, 'success');}/*** 记录销售日志** @param int $productId* @param int $num*/private function recordOrderLog(int $productId, int $num){$sql = "insert into order_test (product_id,sale) values ($productId,$num)";$this->getMySQL()->query($sql);}/*** 获取MySQL连接** @return PDO*/private function getMySQL(){if (false == $this->pdo) {$dsn = 'mysql:host=127.0.0.1;dbname=test';$this->pdo = new \PDO($dsn, 'root', '123456');}return $this->pdo;}/*** 获取Redis连接** @return null|Redis*/private function getRedis(){if (false == $this->redis) {$redis = new Redis();$redis->connect('127.0.0.1', 6379);$redis->auth('haveyb');$this->redis = $redis;}return $this->redis;}/*** 统一响应** @param int $statusCode* @param string $msg* @param array $data* @return string*/private function response(int $statusCode, string $msg, array $data = []){$data = ['status' => $statusCode,'msg'    => $msg,'data'   => $data];return json_encode($data);}/*** 获取日志表中销量总量** @param int $productId* @return string*/public function getSaleSum(int $productId){$sql = 'select sum(sale) from order_test where product_id = '.$productId;$data = $this->getMySQL()->query($sql)->fetch(2);return $this->response(1, 'success', [$data]);}}$object = new OperateStock();
// 先生成商品的队列结构库存,这个数据一定是抢购前就生成好的,而不是查询redis数据查不到时才去生成的,否则并发情况下会出错
//$object->resetStockToRedis(1, 2000);// 生成随机的商品下单数,使用ab压测工具测试并发下是否超卖 ab -n 30000 -c 6000 http://test.cn/
//$num = 1;
$num = rand(1, 299);
print_r($object->placeOrder(1, $num));// 获取订单日志中该商品实际销售总数,主要用于核对校验并发状况下,是否超卖
//print_r($object->getSaleSum(1));

 

方案四:使用Redis事务监听【待补充】

 

 

原文链接:老迟笔记-高并发下防止库存超卖解决方案

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282057.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【几何/分治】【最短路】【数学期望】Day 10.24

1、斜率 可以证明如果两点之间还有一点的话那么原来的两个点连线一定不会是最大斜率 然后我就写了个沙茶分治………… 其实根据上面的推论只用枚举相邻的两个点&#xff0c;扫一遍就可以了 1 #include <cstdio>2 #include <algorithm>3 #include <iostream>4…

K8s 介绍

过去一段时间&#xff0c;公司事情比较多&#xff0c;现在稍微能好点&#xff0c;今天进一步验证自己K8S 集群环境&#xff0c;遇到不少问题&#xff0c; 发现从自己的master 上无法访问node 的pod&#xff0c; 然后一堆search 。 config 。。 [rootk8s-master ~]# systemctl s…

easypoi needmerge失效_EasyPOI简单用例,简单有效

用poi导出Excel表格&#xff0c;需要配置很多东西&#xff0c;也比较麻烦&#xff0c;这里使用poi的封装easypoi&#xff0c;可以快速配置&#xff0c;实现Excel或者word文件的导出。这里我们结合SpringMVC开发easypoi。1&#xff0c;导入以下3个.jar包:这里是springMVC和easyp…

禁止sethc.exe运行 防止3389的sethc后门

废话&#xff1a;在土司看到的一篇文章,发私信给那个哥们儿说让不让转载,结果还没回复我就在百度看到相同的文章。他自己也是转载的。这哥们儿ID迟早被ban 文章转载自:http://www.jb51.net/hack/64484.html 点“开始”&#xff0c;在“运行”中敲入gpedit.msc依次展开“用户配置…

Mac 与虚拟机中的linux集群共享文件目录设置

一、环境介绍 本机&#xff1a;Macos Big Sur系统 虚拟机软件&#xff1a;vmware-fusion 虚拟机上虚拟的linux - centos7 系统 二、实现的效果 在mac上创建一个/Users/SH-Server/vm-vagrant目录&#xff0c;作为之后和虚拟机linux系统 /data 文件夹的共享目录。 我们最终想…

jsp编程技术徐天凤课后答案_jsp编程技术教材课后习题.doc

jsp编程技术教材课后习题JSP编程技术习题集1.6 本 章 习 题思考题(1)为什么要为JDK设置环境变量&#xff1f;(2)Tomcat和JDK是什么关系&#xff1f;(3)什么是Web服务根目录、子目录、相对目录&#xff1f;如何配置虚拟目录&#xff1f;(4)什么是B/S模式&#xff1f;(5)JSP、Jav…

JVM知识(一)

java三大流&#xff1a;数据流、控制流、指令流 线程是执行程序的最小单元&#xff0c;一个线程中也有这些东西。 java 运行时数据区&#xff1a; 1.程序计数器 指向当前线程正在执行的字节码指令地址。如果此时从一个线程转为执行另一个线程&#xff0c;此时就会中断&#xff…

AWD-LSTM为什么这么棒?

摘要&#xff1a; AWD-LSTM为什么这么棒&#xff0c;看完你就明白啦&#xff01;AWD-LSTM是目前最优秀的语言模型之一。在众多的顶会论文中&#xff0c;对字级模型的研究都采用了AWD-LSTMs&#xff0c;并且它在字符级模型中的表现也同样出色。 本文回顾了论文——Regularizing …

Spread / Rest 操作符

Spread / Rest 操作符指的是 ...&#xff0c;具体是 Spread 还是 Rest 需要看上下文语境。 当被用于迭代器中时&#xff0c;它是一个 Spread 操作符&#xff1a;&#xff08;参数为数组&#xff09; function foo(x,y,z) {console.log(x,y,z); }let arr [1,2,3]; foo(...arr);…

python postman脚本自动化_如何用Postman做接口自动化测试

什么是自动化测试把人对软件的测试行为转化为由机器执行测试行为的一种实践。例如GUI自动化测试&#xff0c;模拟人去操作软件界面&#xff0c;把人从简单重复的劳动中解放出来本质是用代码去测试另一段代码&#xff0c;属于一种软件开发工作&#xff0c;已经开发完成的用例还必…

Mac上,为虚拟机集群上的每台虚拟机设置固定IP

一、环境介绍 本机&#xff1a;macOS系统 虚拟机软件&#xff1a;VMware Fusion 虚拟机上&#xff1a;centos7内核的Linux系统集群 二、为什么要为每台虚拟机设置固定ip 由于每次启动虚拟机&#xff0c;得到的ip可能不一样&#xff0c;这样对远程连接非常不友好&#xff0c…

朱晔的互联网架构实践心得S1E7:三十种架构设计模式(上)

设计模式是前人通过大量的实践总结出来的一些经验总结和最佳实践。在经过多年的软件开发实践之后&#xff0c;回过头来去看23种设计模式你会发现很多平时写代码的套路和OO的套路和设计模式里总结的类似&#xff0c;这也说明了你悟到的东西和别人悟到的一样&#xff0c;经过大量…

记一次某制造业ERP系统 CPU打爆事故分析

一&#xff1a;背景 1.讲故事前些天有位朋友微信找到我&#xff0c;说他的程序出现了CPU阶段性爆高&#xff0c;过了一会就下去了&#xff0c;咨询下这个爆高阶段程序内部到底发生了什么&#xff1f;画个图大概是下面这样&#xff0c;你懂的。按经验来说&#xff0c;这种情况一…

PC端和移动APP端CSS样式初始化

CSS样式初始化分为PC端和移动APP端 1.PC端&#xff1a;使用Normalize.css Normalize.css是一种CSS reset的替代方案。 我们创造normalize.css有下面这几个目的&#xff1a; 保护有用的浏览器默认样式而不是完全去掉它们一般化的样式&#xff1a;为大部分HTML元素提供修复浏览器…

FPGA浮点数定点化

因为在普通的fpga芯片里面&#xff0c;寄存器只可以表示无符号型&#xff0c;不可以表示小数&#xff0c;所以在计算比较精确的数值时&#xff0c;就需要做一些处理&#xff0c;不过在altera在Arria 10 中增加了硬核浮点DSP模块&#xff0c;这样更加适合硬件加速和做一些比较精…

框架实现修改功能的原理_JAVA集合框架的特点及实现原理简介

1.集合框架总体架构集合大致分为Set、List、Queue、Map四种体系,其中List,Set,Queue继承自Collection接口&#xff0c;Map为独立接口Set的实现类有:HashSet&#xff0c;LinkedHashSet&#xff0c;TreeSet...List下有ArrayList&#xff0c;Vector&#xff0c;LinkedList...Map下…

NPM报错终极大法

2019独角兽企业重金招聘Python工程师标准>>> 所有的错误基本上都跟node的版本相关 直接删除系统中的node 重新安装 sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node.*} 重新安装 $ n lts $ npm install -g npm $ n stable…

自己使用的一个.NET轻量开发结构

三个文件夹&#xff0c;第一个是放置前端部分&#xff0c;第二个是各种支持的类文件&#xff0c;第三个是单元测试文件。Core文件类库放置的是与数据库做交互的文件&#xff0c;以及一些第三方类库&#xff0c;还有与数据库连接的文件1.Lasy.Validator是一个基于Attribute验证器…

英语影视台词---八、the shawshank redemption

英语影视台词---八、the shawshank redemption 一、总结 一句话总结&#xff1a;肖申克的救赎 1、Its funny. On the outside, I was an honest man. Straight as an arrow. I had to come to prison to be a crook.&#xff1f; 这很有趣。 在外面&#xff0c;我是一个诚实的人…

10.python网络编程(socket server 实现并发 part 2)

一、基于tcp的socket通信的基本原理分析。基于tcp的socket通信&#xff0c;主要依靠两个循环&#xff0c;分别是连接循环和通信循环。这个前面的文章有写过&#xff0c;在这里就不再重复了。二、socketserver实现多并发的原理分析。1.server类&#xff1a;2.reques类。类继承关…