高并发下防止库存超卖解决方案
生活随笔
收集整理的這篇文章主要介紹了
高并发下防止库存超卖解决方案
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
一、概述
目前網(wǎng)上關(guān)于防止庫存超賣,我沒找到可以支持一次購買多件的,都是基于一次只能購買一件做的秒殺方案,但是實(shí)際場景中,一般秒殺活動都是支持1~5件的,因此為了補(bǔ)缺,寫了此文,方便自己之后使用。
二、建表
1、商品表
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 '下單數(shù)量',PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單日志表';3、向商品表內(nèi)插入一條記錄
INSERT INTO product_test (`stock`) VALUES (1300);
三、不同方案下測試報(bào)告
方案一:只將MySQL 庫存字段設(shè)置了無符號
該方案存在超賣情況。測試總請求數(shù)20000,并發(fā)數(shù)8000的情況下,反應(yīng)較慢。該方案在高并發(fā)下不可行,不是高并發(fā)情況下也有風(fēng)險(xiǎn)。
方案二:使用MySQL排它鎖(for update)
方案三:使用Redis隊(duì)列【推薦方案】
方案四:使用Redis事務(wù)監(jiān)聽【待補(bǔ)充】
?
四、代碼部分
方案一、只將庫存字段設(shè)置為無符號
<?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, '超出庫存,無法下單');}// 執(zhí)行扣減庫存操作$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;}/*** 獲取商品庫存數(shù)** @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'];}/*** 統(tǒng)一響應(yīng)** @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);} } // 生成隨機(jī)的商品下單數(shù),使用ab壓測工具測試并發(fā)下是否超賣 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 )方案
在事務(wù)中執(zhí)行,并且在首次查商品剩余庫存時(shí),就將排它鎖加上,注意,是查詢時(shí)就加上,而不是操作時(shí)再加
<?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 {// 開啟事務(wù)$this->getMySQL()->beginTransaction();$stock = $this->getProductStock($productId);if ($num > $stock) {return $this->response(0, '超出庫存,無法下單');}// 執(zhí)行扣減庫存操作$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;}/*** 獲取商品庫存數(shù)** @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'];}/*** 統(tǒng)一響應(yīng)** @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]);}} // 生成隨機(jī)的商品下單數(shù),使用ab壓測工具測試并發(fā)下是否超賣 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 隊(duì)列方案【推薦】
?
方案四:使用Redis事務(wù)監(jiān)聽【待補(bǔ)充】
?
?
原文鏈接:老遲筆記-高并發(fā)下防止庫存超賣解決方案
總結(jié)
以上是生活随笔為你收集整理的高并发下防止库存超卖解决方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 20分钟完成Mac上的 LNMP 环境部
- 下一篇: Mac上,为虚拟机集群上的每台虚拟机设置