首頁  >  文章  >  後端開發  >  PHP高併發情形下怎麼防止商品庫存超賣

PHP高併發情形下怎麼防止商品庫存超賣

WBOY
WBOY轉載
2022-04-06 10:37:107716瀏覽

這篇文章為大家帶來了關於PHP的相關知識,其中主要介紹了關於在高並發情況下防止商品庫存超賣的相關問題,主要解決高並發對資料庫產生的壓力以及競爭狀態如何解決商品庫存超賣,希望對大家有幫助。

PHP高併發情形下怎麼防止商品庫存超賣

可以透過《php高並發測試:防止庫存超賣的案例講解》查看基於本篇文章的測試案例。 【推薦學習:《PHP教學》】

商城系統中,搶購和秒殺是很常見的行銷場景,在一定時間內有大量的使用者造訪商場下單,主要需要解決的問題有兩個:

  • 高並發對資料庫產生的壓力;

  • ##在競爭狀態下如何解決商品庫存超賣;

高並發對資料庫產生的壓力

對於第一個問題,使用快取來處理,避免直接操作資料庫,例如使用Redis。

競爭狀態下如何解決商品庫存超賣

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

常規寫法:查詢出對應商品的庫存,判斷庫存數量否大於0,然後執行生成訂單等操作,但是在判斷庫存是否大於0 處,如果在高並發下就會有問題,導致庫存量出現負數。

測試表sql

把如下表資料匯入到資料庫中

/*
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[&#39;store&#39;] > 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(&#39;库存减少成功,下单成功&#39;);
    } else {
        echo "更新失败";
        insertLog(&#39;库存减少失败&#39;);
    }

} else {
    echo "没有库存";
    insertLog(&#39;库存不够&#39;);
}

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

將庫存欄位欄位設為unsigned

因為庫存欄位不能為負數,下單後更新商品庫存時,如果出現負數將返回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[&#39;store&#39;] > 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(&#39;库存减少成功,下单成功&#39;);
    } else {
        // 如果出现负数将返回false
        echo "更新失败";
        insertLog(&#39;库存减少失败&#39;);
    }
} else {
    //商品已经抢购完
    echo "没有库存";
    insertLog(&#39;库存不够&#39;);
}

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

使用mysql 的事務,鎖住操作的行

在下單處理過程中,使用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[&#39;store&#39;] > 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(&#39;库存减少成功,下单成功&#39;);
        mysqli_query($con, "COMMIT");//事务提交即解锁
    } else {
        echo "更新失败";
        insertLog(&#39;库存减少失败&#39;);
        mysqli_query($con, "ROLLBACK");//事务回滚即解锁
    }
} else {
    //商品已经抢购完
    echo "没有库存";
    insertLog(&#39;库存不够&#39;);
    mysqli_query($con, "ROLLBACK");//事务回滚即解锁
}

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

#使用非阻塞的檔案排他鎖

在處理下單請求的時候,用flock 鎖定一個文件,如果鎖定失敗說明有其他訂單正在處理,此時要么等待要么直接提示用戶” 伺服器繁忙”,計數器存儲搶購的商品數量,避免查詢數據庫。

阻塞 (等待) 模式:並發時,當有第二個使用者要求時,會等待第一個使用者要求完成、釋放鎖,取得檔案鎖之後,程式才會繼續運作。

<?php
db();
global $con;

//step1 接收下单参数
$product_id = 1;// 商品ID
$buy_num = 1;// 购买数量

$fp = fopen(&#39;lock.txt&#39;, &#39;w&#39;);
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[&#39;store&#39;] > 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(&#39;库存减少成功,下单成功&#39;);
        } else {
            echo "更新失败";
            insertLog(&#39;库存减少失败&#39;);
        }
    } else {
        //商品已经抢购完
        echo "没有库存";
        insertLog(&#39;库存不够&#39;);
    }
    flock($fp, LOCK_UN); //释放锁

}
fclose($fp);

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

非阻塞模式:並發時,第一個使用者請求,拿得檔案鎖定之後。後面請求的使用者直接返回系統繁忙,請稍後再試

<?php
db();
global $con;

//step1 接收下单参数
$product_id = 1;// 商品ID
$buy_num = 1;// 购买数量

$fp = fopen(&#39;lock.txt&#39;, &#39;w&#39;);
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[&#39;store&#39;] > 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(&#39;库存减少成功,下单成功&#39;);
        } else {
            echo "更新失败";
            insertLog(&#39;库存减少失败&#39;);
        }
    } else {
        //商品已经抢购完
        echo "没有库存";
        insertLog(&#39;库存不够&#39;);
    }
    flock($fp, LOCK_UN); //释放锁

} else {
    //系统繁忙,请稍后再试
    echo "系统繁忙,请稍后再试";
    insertLog(&#39;系统繁忙,请稍后再试&#39;);
}
fclose($fp);

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

使用redis 佇列

  • 因為pop操作是原子的,即使有很多用戶同時到達,也是依次執行,推薦使用


  • #mysql 事務在高並發下性能下降很厲害,文件鎖的方式也是

1.先將商品庫存到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[&#39;store&#39;];

// 获取商品在redis缓存的库存
$redis = new Redis();
$result = $redis->connect(&#39;127.0.0.1&#39;, 6379);
$key = &#39;goods_store_&#39; . $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(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

2. 搶購、秒殺邏輯

<?php

db();
global $con;

//step1 接收下单参数
$product_id = 1;// 商品ID
$buy_num = 1;// 购买数量

//step2 下单前判断redis队列库存量
$redis = new Redis();
$result = $redis->connect(&#39;127.0.0.1&#39;,6379);
$count = $redis->lpop(&#39;goods_store_&#39; . $product_id);
if (!$count) {
    insertLog(&#39;error:no store redis&#39;);
    return &#39;秒杀结束,没有商品库存了&#39;;
}

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(&#39;库存减少成功,下单成功&#39;);
} else {
    echo "更新失败";
    insertLog(&#39;库存减少失败&#39;);
}

function db()
{
    global $con;
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    if (!$con) {
        echo "数据库连接失败";
    }
}

/**
 * 生成唯一订单号
 */
function build_order_no()
{
    return date(&#39;Ymd&#39;) . str_pad(mt_rand(1, 99999), 5, &#39;0&#39;, STR_PAD_LEFT);
}

function create_order($oid, $product_id, $number)
{
    global $con;
    $sql = "INSERT INTO `order` (oid, product_id, number) values(&#39;$oid&#39;, &#39;$product_id&#39;, &#39;$number&#39;)";
    mysqli_query($con, $sql);
}

/**
 * 记录日志
 */
function insertLog($content)
{
    global $con;
    $sql = "INSERT INTO `order_log` (content) values(&#39;$content&#39;)";
    mysqli_query($con, $sql);
}

redis 樂觀鎖定防止超賣

<?php

$redis =new Redis();
$redis->connect("127.0.0.1", 6379);
$redis->watch(&#39;sales&#39;);//乐观锁 监视作用 set()  初始值0
$sales = $redis->get(&#39;sales&#39;);

$n = 20;// 库存
if ($sales >= $n) {
    exit(&#39;秒杀结束&#39;);
}

//redis开启事务
$redis->multi();
$redis->incr(&#39;sales&#39;); //将 key 中储存的数字值增一 ,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
$res = $redis->exec(); //成功1 失败0

if ($res) {
    //秒杀成功
    $con = new mysqli(&#39;localhost&#39;,&#39;root&#39;,&#39;root&#39;,&#39;test&#39;);
    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(&#39;抢购失败&#39;);
}

推薦學習:《

PHP影片教學

以上是PHP高併發情形下怎麼防止商品庫存超賣的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除