您好,登錄后才能下訂單哦!
這篇文章主要介紹“PHP高并發之怎么解決商品庫存超賣問題”,在日常操作中,相信很多人在PHP高并發之怎么解決商品庫存超賣問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”PHP高并發之怎么解決商品庫存超賣問題”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
對于第一個問題,使用緩存來處理,避免直接操作數據庫,例如使用 Redis。
對于第二個問題,需要重點說明。
常規寫法:查詢出對應商品的庫存,判斷庫存數量否大于 0,然后執行生成訂單等操作,但是在判斷庫存是否大于 0 處,如果在高并發下就會有問題,導致庫存量出現負數。
把如下表數據導入到數據庫中
/* Navicat MySQL Data Transfer Source Server : 01 本地localhost Source Server Version : 50553 Source Host : localhost:3306 Source Database : test Target Server Type : MYSQL Target Server Version : 50553 File Encoding : 65001 Date: 2020-11-06 14:31:35 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for products -- ---------------------------- DROP TABLE IF EXISTS `products`; CREATE TABLE `products` ( `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID', `title` varchar(50) DEFAULT NULL COMMENT '貨品名稱', `store` int(11) DEFAULT '0' COMMENT '貨品庫存', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='貨品表'; -- ---------------------------- -- Records of products -- ---------------------------- INSERT INTO `products` VALUES ('1', '稻花香大米', '20'); -- ---------------------------- -- Table structure for order_log -- ---------------------------- DROP TABLE IF EXISTS `order_log`; CREATE TABLE `order_log` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `content` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '日志內容', `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; -- ---------------------------- -- Table structure for order -- ---------------------------- DROP TABLE IF EXISTS `order`; CREATE TABLE `order` ( `oid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '訂單號', `product_id` int(11) DEFAULT '0' COMMENT '商品ID', `number` int(11) DEFAULT '0' COMMENT '購買數量', `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間', PRIMARY KEY (`oid`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='訂單表';
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 //step2 查詢商品信息 $sql = "select * from products where id={$product_id}"; $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); //step3 判斷商品下單數量是否大于商品庫存數量 //此處在高并發下,可能出現上一個下單后還沒來得及更新庫存,下一個下單判斷庫存數不是最新的庫存 if ($row['store'] > 0) { sleep(1); //step4 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step5 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); } else { echo "更新失敗"; insertLog('庫存減少失敗'); } } else { echo "沒有庫存"; insertLog('庫存不夠'); } function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
因為庫存字段不能為負數,在下單后更新商品庫存時,如果出現負數將返回 false
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 //step2 查詢商品信息 $sql = "select * from products where id={$product_id} for UPDATE";//利用for update 開啟行鎖 $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); //step3 判斷商品下單數量是否大于商品庫存數量 if ($row['store'] > 0) { sleep(1); //step4 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step5 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); } else { // 如果出現負數將返回false echo "更新失敗"; insertLog('庫存減少失敗'); } } else { //商品已經搶購完 echo "沒有庫存"; insertLog('庫存不夠'); } function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
在下單處理過程中,使用 mysql 的事務將正在下單商品行數據鎖定
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 mysqli_query($con, "BEGIN"); //開始事務 //step2 查詢商品信息 $sql = "select * from products where id={$product_id} for UPDATE";//利用for update 開啟行鎖 $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); //step3 判斷商品下單數量是否大于商品庫存數量 if ($row['store'] > 0) { sleep(1); //step4 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step5 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); mysqli_query($con, "COMMIT");//事務提交即解鎖 } else { echo "更新失敗"; insertLog('庫存減少失敗'); mysqli_query($con, "ROLLBACK");//事務回滾即解鎖 } } else { //商品已經搶購完 echo "沒有庫存"; insertLog('庫存不夠'); mysqli_query($con, "ROLLBACK");//事務回滾即解鎖 } function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
在處理下單請求的時候,用 flock 鎖定一個文件,如果鎖定失敗說明有其他訂單正在處理,此時要么等待要么直接提示用戶” 服務器繁忙”,計數器存儲搶購的商品數量,避免查詢數據庫。
阻塞 (等待) 模式:并發時,當有第二個用戶請求時,會等待第一個用戶請求完成、釋放鎖,獲得文件鎖之后,程序才會繼續運行下去。
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 $fp = fopen('lock.txt', 'w'); if (flock($fp, LOCK_EX)) { //文件獨占鎖,阻塞 //step2 查詢商品信息 $sql = "select * from products where id={$product_id}"; $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); //step3 判斷商品下單數量是否大于商品庫存數量 if ($row['store'] > 0) { //處理訂單 sleep(1); //step4 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step5 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); } else { echo "更新失敗"; insertLog('庫存減少失敗'); } } else { //商品已經搶購完 echo "沒有庫存"; insertLog('庫存不夠'); } flock($fp, LOCK_UN); //釋放鎖 } fclose($fp); function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
非阻塞模式:并發時,第一個用戶請求,拿得文件鎖之后。后面請求的用戶直接返回系統繁忙,請稍后再試
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 $fp = fopen('lock.txt', 'w'); if (flock($fp, LOCK_EX|LOCK_NB)) { //文件獨占鎖,非阻塞 //step2 查詢商品信息 $sql = "select * from products where id={$product_id}"; $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); //step3 判斷商品下單數量是否大于商品庫存數量 if ($row['store'] > 0) { //處理訂單 sleep(1); //step4 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step5 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); } else { echo "更新失敗"; insertLog('庫存減少失敗'); } } else { //商品已經搶購完 echo "沒有庫存"; insertLog('庫存不夠'); } flock($fp, LOCK_UN); //釋放鎖 } else { //系統繁忙,請稍后再試 echo "系統繁忙,請稍后再試"; insertLog('系統繁忙,請稍后再試'); } fclose($fp); function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
因為 pop 操作是原子的,即使有很多用戶同時到達,也是依次執行,推薦使用
mysql 事務在高并發下性能下降很厲害,文件鎖的方式也是
先將商品庫存到 redis 隊列
<?php db(); global $con; // 查詢商品信息 $product_id = 1; $sql = "select * from products where id={$product_id}"; $result = mysqli_query($con, $sql); $row = mysqli_fetch_assoc($result); $store = $row['store']; // 獲取商品在redis緩存的庫存 $redis = new Redis(); $result = $redis->connect('127.0.0.1', 6379); $key = 'goods_store_' . $product_id; $res = $redis->llen($key); $count = $store - $res; for ($i=0; $i<$count; $i++) { $redis->lpush($key, 1); } echo $redis->llen($key); function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } }
2. 搶購、秒殺邏輯
<?php db(); global $con; //step1 接收下單參數 $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 //step2 下單前判斷redis隊列庫存量 $redis = new Redis(); $result = $redis->connect('127.0.0.1',6379); $count = $redis->lpop('goods_store_' . $product_id); if (!$count) { insertLog('error:no store redis'); return '秒殺結束,沒有商品庫存了'; } sleep(1); //step3 更新商品庫存數量(減去下單數量) $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "更新成功"; //step4 生成訂單號創建訂單 $oid = build_order_no(); create_order($oid, $product_id, $buy_num); insertLog('庫存減少成功,下單成功'); } else { echo "更新失敗"; insertLog('庫存減少失敗'); } function db() { global $con; $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } } /** * 生成唯一訂單號 */ function build_order_no() { return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); } function create_order($oid, $product_id, $number) { global $con; $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')"; mysqli_query($con, $sql); } /** * 記錄日志 */ function insertLog($content) { global $con; $sql = "INSERT INTO `order_log` (content) values('$content')"; mysqli_query($con, $sql); }
redis 樂觀鎖防止超賣
<?php $redis =new Redis(); $redis->connect("127.0.0.1", 6379); $redis->watch('sales');//樂觀鎖 監視作用 set() 初始值0 $sales = $redis->get('sales'); $n = 20;// 庫存 if ($sales >= $n) { exit('秒殺結束'); } //redis開啟事務 $redis->multi(); $redis->incr('sales'); //將 key 中儲存的數字值增一 ,如果 key 不存在,那么 key 的值會先被初始化為 0 ,然后再執行 INCR 操作。 $res = $redis->exec(); //成功1 失敗0 if ($res) { //秒殺成功 $con = new mysqli('localhost','root','root','test'); if (!$con) { echo "數據庫連接失敗"; } $product_id = 1;// 商品ID $buy_num = 1;// 購買數量 sleep(1); $sql = "update products set store=store-{$buy_num} where id={$product_id}"; if (mysqli_query($con, $sql)) { echo "秒殺完成"; } } else { exit('搶購失敗'); }
到此,關于“PHP高并發之怎么解決商品庫存超賣問題”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。