PHP秒杀防止超卖方法

wen PHP项目 1

本文目录导读:

PHP秒杀防止超卖方法

  1. 方案一:数据库乐观锁(推荐,最稳妥)
  2. 方案二:Redis 原子递减(性能最强,最常用)
  3. 方案三:数据库悲观锁(FOR UPDATE)(简单,适合中小型项目)
  4. 方案四:Redis + Lua 脚本(最终推荐方案)
  5. 关键注意事项(避免踩坑)

PHP 秒杀防止超卖的核心在于保证库存扣减的原子性,避免在高并发下多个请求同时读到相同的库存并成功扣减。

下面按从简单到复杂、从数据库到缓存的顺序,列出几种最有效的解决方案,并附带关键代码示例。


数据库乐观锁(推荐,最稳妥)

原理:在更新库存时,不仅检查 库存 > 0,还检查 版本号当前库存 是否等于预期值,如果条件不满足,更新影响行数为0,则说明库存已被抢走。

核心SQL(关键)

UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = ? 
  AND stock > 0  -- 防止超卖的第一道保险
  AND version = ?; -- 乐观锁,防止并发覆盖

PHP + PDO 代码示例

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$productId = 1;
$userId = 100;
try {
    $pdo->beginTransaction();
    // 1. 读取当前版本号(模拟乐观锁)
    $stmt = $pdo->prepare("SELECT stock, version FROM products WHERE id = ? FOR UPDATE");
    $stmt->execute([$productId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if ($row && $row['stock'] > 0) {
        // 2. 执行扣减(带上版本号条件)
        $update = $pdo->prepare("UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ? AND stock > 0");
        $update->execute([$productId, $row['version']]);
        // 影响行数为1表示成功
        if ($update->rowCount() === 1) {
            // 3. 记录订单(业务逻辑)
            $pdo->prepare("INSERT INTO orders (user_id, product_id) VALUES (?, ?)")->execute([$userId, $productId]);
            $pdo->commit();
            echo "秒杀成功!";
        } else {
            $pdo->rollBack();
            echo "库存不足或已被抢光!";
        }
    } else {
        $pdo->rollBack();
        echo "商品不存在或库存为空!";
    }
} catch (Exception $e) {
    $pdo->rollBack();
    echo "系统错误:" . $e->getMessage();
}
?>

优点:绝对保证一致性,无需额外组件。 缺点:数据库压力大,不适合极大规模并发(如双十一)。


Redis 原子递减(性能最强,最常用)

原理:利用 Redis 单线程的特性,DECRINCR 操作天然是原子的。
流程:秒杀前将库存预先加载到 Redis,用户请求时先执行 DECR,如果返回值大于等于0则抢购成功,否则失败并恢复库存。

Redis 核心操作

# 预加载库存
SET stock:product:1 100
# 扣减库存(原子)
DECR stock:product:1

PHP 代码示例(使用 Redis + Lua 脚本保证原子性)

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$productId = 1;
$userId = 100;
// 预先设置库存,例如100个
$redis->set('stock:product:' . $productId, 100);
// Lua 脚本:保证检查库存和扣减库存是原子操作
$luaScript = <<<LUA
    local stock = redis.call('GET', KEYS[1])
    if tonumber(stock) > 0 then
        redis.call('DECR', KEYS[1])
        return 1 -- 成功
    else
        return 0 -- 失败(库存不足)
    end
LUA;
// 使用 eval 执行
$result = $redis->eval($luaScript, ['stock:product:' . $productId], 1);
if ($result === 1) {
    // 异步写入数据库或消息队列生成订单
    echo "秒杀成功,库存剩余:" . $redis->get('stock:product:' . $productId);
} else {
    echo "秒杀失败,库存已空";
}
?>

优点:每秒可处理十几万次请求,性能极高。
缺点:需要维护 Redis 与数据库的最终一致性(需结合消息队列或定时任务同步)。


数据库悲观锁(FOR UPDATE)(简单,适合中小型项目)

原理:在读取库存时直接对记录行加锁,直到事务提交才释放,期间其他事务无法修改该行。

PHP 代码

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$pdo->beginTransaction();
$productId = 1;
$userId = 100;
try {
    // 锁定该行,直到事务结束
    $stmt = $pdo->prepare("SELECT stock FROM products WHERE id = ? FOR UPDATE");
    $stmt->execute([$productId]);
    $row = $stmt->fetch();
    if ($row && $row['stock'] > 0) {
        // 扣减库存
        $pdo->prepare("UPDATE products SET stock = stock - 1 WHERE id = ?")->execute([$productId]);
        // 创建订单
        $pdo->prepare("INSERT INTO orders ...")->execute([...]);
        $pdo->commit();
        echo "成功";
    } else {
        $pdo->rollBack();
        echo "没货了";
    }
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}
?>

注意:必须使用存储引擎 InnoDB(MySQL默认)才支持行锁,此方案在高并发下会产生大量阻塞等待,性能低于乐观锁。


Redis + Lua 脚本(最终推荐方案)

如果要求既要高性能又要绝对不超卖,将 Redis递减数据库异步扣库存 结合是行业标准做法。

完整流程

  1. 预加载库存:秒杀开始前,将库存写入 Redis。
  2. 抢购命中:使用 Lua 脚本原子扣减 Redis 库存。
  3. 异步写库:扣减成功后,发送消息到 RabbitMQ/Kafka,或直接写入一个 订单队列表
  4. 后台任务同步:消费队列,将库存更新到数据库,并生成真实订单。

关键注意事项(避免踩坑)

坑点 解决方案
数据库字段类型 库存字段建议用 INT UNSIGNED 且默认值0,防止出现负数。
使用行锁(FOR UPDATE)时 必须确保扫描索引,否则会锁全表(WHERE id=? 必须有索引)。
Redis 与 DB 一致性问题 秒杀成功后,无需立即同步数据库,可延迟同步;失败则定时回滚 Redis 库存(加回)。
接口幂等性 同一个用户重复点击,需要在 Redis 用 SETNX 记录用户已购买,防止重复下单。

最终建议

  • 中小规模(日活 < 10万):直接使用 乐观锁(方案一),简单可靠。
  • 大规模(双十一级别):使用 Redis + Lua(方案二/四),但需要额外开发数据同步逻辑。

确保库存不超卖的原理都是利用单点原子操作(数据库行锁/Redis单线程)来防止多个请求同时通过“库存检查”这一关。

抱歉,评论功能暂时关闭!