PHP高并发之怎么解决商品库存超卖问题

这篇文章主要介绍“PHP高并发之怎么解决商品库存超卖问题”,在日常操作中,相信很多人在PHP高并发之怎么解决商品库存超卖问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”PHP高并发之怎么解决商品库存超卖问题”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

成都创新互联专业为企业提供惠安网站建设、惠安做网站、惠安网站设计、惠安网站制作等企业网站建设、网页设计与制作、惠安企业网站模板建站服务,10余年惠安做网站经验,不只是建网站,更提供有价值的思路和整体网络服务。

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='订单表';

下单处理代码

 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);
}

将库存字段字段设为 unsigned

因为库存字段不能为负数,在下单后更新商品库存时,如果出现负数将返回 false

 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 的事务,锁住操作的行

在下单处理过程中,使用 mysql 的事务将正在下单商品行数据锁定

 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 锁定一个文件,如果锁定失败说明有其他订单正在处理,此时要么等待要么直接提示用户” 服务器繁忙”,计数器存储抢购的商品数量,避免查询数据库。

阻塞 (等待) 模式:并发时,当有第二个用户请求时,会等待第一个用户请求完成、释放锁,获得文件锁之后,程序才会继续运行下去。

 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);
}

非阻塞模式:并发时,第一个用户请求,拿得文件锁之后。后面请求的用户直接返回系统繁忙,请稍后再试

 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 队列

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. 抢购、秒杀逻辑

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 乐观锁防止超卖

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高并发之怎么解决商品库存超卖问题”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注创新互联网站,小编会继续努力为大家带来更多实用的文章!


文章名称:PHP高并发之怎么解决商品库存超卖问题
文章URL:http://myzitong.com/article/pedsch.html