溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

PHP高并發(fā)之怎么解決商品庫存超賣問題

發(fā)布時間:2022-04-06 11:04:42 來源:億速云 閱讀:484 作者:iii 欄目:編程語言

這篇文章主要介紹“PHP高并發(fā)之怎么解決商品庫存超賣問題”,在日常操作中,相信很多人在PHP高并發(fā)之怎么解決商品庫存超賣問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”PHP高并發(fā)之怎么解決商品庫存超賣問題”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

PHP高并發(fā)之怎么解決商品庫存超賣問題

高并發(fā)對數(shù)據(jù)庫產(chǎn)生的壓力

對于第一個問題,使用緩存來處理,避免直接操作數(shù)據(jù)庫,例如使用 Redis。

競爭狀態(tài)下如何解決商品庫存超賣

對于第二個問題,需要重點說明。

常規(guī)寫法:查詢出對應商品的庫存,判斷庫存數(shù)量否大于 0,然后執(zhí)行生成訂單等操作,但是在判斷庫存是否大于 0 處,如果在高并發(fā)下就會有問題,導致庫存量出現(xiàn)負數(shù)。

測試表 sql

把如下表數(shù)據(jù)導入到數(shù)據(jù)庫中

/*
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 '日志內(nèi)容',
  `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  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 '購買數(shù)量',
  `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  PRIMARY KEY (`oid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='訂單表';

下單處理代碼

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
//step2 查詢商品信息
$sql = "select * from products where id={$product_id}";
$result = mysqli_query($con, $sql);
$row = mysqli_fetch_assoc($result);
//step3 判斷商品下單數(shù)量是否大于商品庫存數(shù)量
//此處在高并發(fā)下,可能出現(xiàn)上一個下單后還沒來得及更新庫存,下一個下單判斷庫存數(shù)不是最新的庫存
if ($row['store'] > 0) {
    sleep(1);
    //step4 更新商品庫存數(shù)量(減去下單數(shù)量)
    $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    if (mysqli_query($con, $sql)) {
        echo "更新成功";
        //step5 生成訂單號創(chuàng)建訂單
        $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 "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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);
}

將庫存字段字段設為 unsigned

因為庫存字段不能為負數(shù),在下單后更新商品庫存時,如果出現(xiàn)負數(shù)將返回 false

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
//step2 查詢商品信息
$sql = "select * from products where id={$product_id} for UPDATE";//利用for update 開啟行鎖
$result = mysqli_query($con, $sql);
$row = mysqli_fetch_assoc($result);
//step3 判斷商品下單數(shù)量是否大于商品庫存數(shù)量
if ($row['store'] > 0) {
    sleep(1);
    //step4 更新商品庫存數(shù)量(減去下單數(shù)量)
    $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    if (mysqli_query($con, $sql)) {
        echo "更新成功";
        //step5 生成訂單號創(chuàng)建訂單
        $oid = build_order_no();
        create_order($oid, $product_id, $buy_num);
        insertLog('庫存減少成功,下單成功');
    } else {
        // 如果出現(xiàn)負數(shù)將返回false
        echo "更新失敗";
        insertLog('庫存減少失敗');
    }
} else {
    //商品已經(jīng)搶購完
    echo "沒有庫存";
    insertLog('庫存不夠');
}
function db()
{
    global $con;
    $con = new mysqli('localhost','root','root','test');
    if (!$con) {
        echo "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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 的事務,鎖住操作的行

在下單處理過程中,使用 mysql 的事務將正在下單商品行數(shù)據(jù)鎖定

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
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 判斷商品下單數(shù)量是否大于商品庫存數(shù)量
if ($row['store'] > 0) {
    sleep(1);
    //step4 更新商品庫存數(shù)量(減去下單數(shù)量)
    $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    if (mysqli_query($con, $sql)) {
        echo "更新成功";
        //step5 生成訂單號創(chuàng)建訂單
        $oid = build_order_no();
        create_order($oid, $product_id, $buy_num);
        insertLog('庫存減少成功,下單成功');
        mysqli_query($con, "COMMIT");//事務提交即解鎖
    } else {
        echo "更新失敗";
        insertLog('庫存減少失敗');
        mysqli_query($con, "ROLLBACK");//事務回滾即解鎖
    }
} else {
    //商品已經(jīng)搶購完
    echo "沒有庫存";
    insertLog('庫存不夠');
    mysqli_query($con, "ROLLBACK");//事務回滾即解鎖
}
function db()
{
    global $con;
    $con = new mysqli('localhost','root','root','test');
    if (!$con) {
        echo "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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 鎖定一個文件,如果鎖定失敗說明有其他訂單正在處理,此時要么等待要么直接提示用戶” 服務器繁忙”,計數(shù)器存儲搶購的商品數(shù)量,避免查詢數(shù)據(jù)庫。

阻塞 (等待) 模式:并發(fā)時,當有第二個用戶請求時,會等待第一個用戶請求完成、釋放鎖,獲得文件鎖之后,程序才會繼續(xù)運行下去。

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
$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 判斷商品下單數(shù)量是否大于商品庫存數(shù)量
    if ($row['store'] > 0) {
        //處理訂單
        sleep(1);
        //step4 更新商品庫存數(shù)量(減去下單數(shù)量)
        $sql = "update products set store=store-{$buy_num} where id={$product_id}";
        if (mysqli_query($con, $sql)) {
            echo "更新成功";
            //step5 生成訂單號創(chuàng)建訂單
            $oid = build_order_no();
            create_order($oid, $product_id, $buy_num);
            insertLog('庫存減少成功,下單成功');
        } else {
            echo "更新失敗";
            insertLog('庫存減少失敗');
        }
    } else {
        //商品已經(jīng)搶購完
        echo "沒有庫存";
        insertLog('庫存不夠');
    }
    flock($fp, LOCK_UN); //釋放鎖
}
fclose($fp);
function db()
{
    global $con;
    $con = new mysqli('localhost','root','root','test');
    if (!$con) {
        echo "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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);
}

非阻塞模式:并發(fā)時,第一個用戶請求,拿得文件鎖之后。后面請求的用戶直接返回系統(tǒng)繁忙,請稍后再試

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
$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 判斷商品下單數(shù)量是否大于商品庫存數(shù)量
    if ($row['store'] > 0) {
        //處理訂單
        sleep(1);
        //step4 更新商品庫存數(shù)量(減去下單數(shù)量)
        $sql = "update products set store=store-{$buy_num} where id={$product_id}";
        if (mysqli_query($con, $sql)) {
            echo "更新成功";
            //step5 生成訂單號創(chuàng)建訂單
            $oid = build_order_no();
            create_order($oid, $product_id, $buy_num);
            insertLog('庫存減少成功,下單成功');
        } else {
            echo "更新失敗";
            insertLog('庫存減少失敗');
        }
    } else {
        //商品已經(jīng)搶購完
        echo "沒有庫存";
        insertLog('庫存不夠');
    }
    flock($fp, LOCK_UN); //釋放鎖
} else {
    //系統(tǒng)繁忙,請稍后再試
    echo "系統(tǒng)繁忙,請稍后再試";
    insertLog('系統(tǒng)繁忙,請稍后再試');
}
fclose($fp);
function db()
{
    global $con;
    $con = new mysqli('localhost','root','root','test');
    if (!$con) {
        echo "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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 操作是原子的,即使有很多用戶同時到達,也是依次執(zhí)行,推薦使用

mysql 事務在高并發(fā)下性能下降很厲害,文件鎖的方式也是

先將商品庫存到 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 "數(shù)據(jù)庫連接失敗";
    }
}

2. 搶購、秒殺邏輯

<?php
db();
global $con;
//step1 接收下單參數(shù)
$product_id = 1;// 商品ID
$buy_num = 1;// 購買數(shù)量
//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 更新商品庫存數(shù)量(減去下單數(shù)量)
$sql = "update products set store=store-{$buy_num} where id={$product_id}";
if (mysqli_query($con, $sql)) {
    echo "更新成功";
    //step4 生成訂單號創(chuàng)建訂單
    $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 "數(shù)據(jù)庫連接失敗";
    }
}
/**
 * 生成唯一訂單號
 */
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');//樂觀鎖 監(jiān)視作用 set()  初始值0
$sales = $redis->get('sales');
$n = 20;// 庫存
if ($sales >= $n) {
    exit('秒殺結束');
}
//redis開啟事務
$redis->multi();
$redis->incr('sales'); //將 key 中儲存的數(shù)字值增一 ,如果 key 不存在,那么 key 的值會先被初始化為 0 ,然后再執(zhí)行 INCR 操作。
$res = $redis->exec(); //成功1 失敗0
if ($res) {
    //秒殺成功
    $con = new mysqli('localhost','root','root','test');
    if (!$con) {
        echo "數(shù)據(jù)庫連接失敗";
    }
    $product_id = 1;// 商品ID
    $buy_num = 1;// 購買數(shù)量
    sleep(1);
    $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    if (mysqli_query($con, $sql)) {
        echo "秒殺完成";
    }
} else {
    exit('搶購失敗');
}

到此,關于“PHP高并發(fā)之怎么解決商品庫存超賣問題”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

php
AI