首页 后端开发 php教程 PHP高并发情形下怎么防止商品库存超卖

PHP高并发情形下怎么防止商品库存超卖

Apr 06, 2022 am 10:37 AM
php

本篇文章给大家带来了关于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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 Dec 24, 2024 pm 04:42 PM

PHP 8.4 带来了多项新功能、安全性改进和性能改进,同时弃用和删除了大量功能。 本指南介绍了如何在 Ubuntu、Debian 或其衍生版本上安装 PHP 8.4 或升级到 PHP 8.4

CakePHP 使用数据库 CakePHP 使用数据库 Sep 10, 2024 pm 05:25 PM

在 CakePHP 中使用数据库非常容易。本章我们将了解CRUD(创建、读取、更新、删除)操作。

CakePHP 日期和时间 CakePHP 日期和时间 Sep 10, 2024 pm 05:27 PM

为了在 cakephp4 中处理日期和时间,我们将使用可用的 FrozenTime 类。

CakePHP 文件上传 CakePHP 文件上传 Sep 10, 2024 pm 05:27 PM

为了进行文件上传,我们将使用表单助手。这是文件上传的示例。

讨论 CakePHP 讨论 CakePHP Sep 10, 2024 pm 05:28 PM

CakePHP 是 PHP 的开源框架。它的目的是使应用程序的开发、部署和维护变得更加容易。 CakePHP 基于类似 MVC 的架构,功能强大且易于掌握。模型、视图和控制器 gu

CakePHP 创建验证器 CakePHP 创建验证器 Sep 10, 2024 pm 05:26 PM

可以通过在控制器中添加以下两行来创建验证器。

CakePHP 日志记录 CakePHP 日志记录 Sep 10, 2024 pm 05:26 PM

登录 CakePHP 是一项非常简单的任务。您只需使用一项功能即可。您可以记录任何后台进程(如 cronjob)的错误、异常、用户活动、用户采取的操作。在 CakePHP 中记录数据很容易。提供了 log() 函数

如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 Dec 20, 2024 am 11:31 AM

Visual Studio Code,也称为 VS Code,是一个免费的源代码编辑器 - 或集成开发环境 (IDE) - 可用于所有主要操作系统。 VS Code 拥有针对多种编程语言的大量扩展,可以轻松编写

See all articles