日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2023/12/4 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高并发下防止库存超卖解决方案 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、概述

目前網上關于防止庫存超賣,我沒找到可以支持一次購買多件的,都是基于一次只能購買一件做的秒殺方案,但是實際場景中,一般秒殺活動都是支持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事務監聽【待補充】

?

?

原文鏈接:老遲筆記-高并發下防止庫存超賣解決方案

總結

以上是生活随笔為你收集整理的高并发下防止库存超卖解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。