首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 高併發秒殺/搶購:解決庫存超賣(負數)的四大方案深度對比

     編輯:PHP 綜合知識

問題重現:競態條件

// 典型錯誤流程
$stock = $db->query("SELECT stock FROM products WHERE id=1")->fetchColumn();
if ($stock > 0) {
    $db->exec("UPDATE products SET stock = stock - 1 WHERE id=1");
    // 建立訂單...
}
// 併發時:兩請求同時讀到 stock=1,皆通過判斷,最終 stock=-1

方案對比總覽

方案核心機制優點缺點適用場景
1. 隊列單消費者請求入隊,單進程序列化處理邏輯簡單、絕對不超賣延遲高、需額外 Worker、擴展難非即時業務、後台異步處理
2. 資料庫樂觀鎖版本號/條件更新 + 重試無鎖開銷、資料庫原生支援高衝突時重試多、應用層複雜中低衝突、已有版本欄位
3. 條件更新(推薦)UPDATE ... WHERE stock > 0原子操作、無鎖、效能最佳、程式碼最少需確認 affected_rows高併發扣減庫存、計數器
4. 檔案鎖 flock進程級獨佔鎖跨資料庫、簡單部署單機限制、效能瓶頸、Nfs 不支援單機部署、簡單專案、無 Redis
5. Redis Lua 腳本原子 DEC + 判斷極高效能、分散式支援、功能豐富引入 Redis 依賴、需維護同步高併發、分散式、需限流

方案三:條件更新(最推薦用於關係型資料庫)

function deductStock(PDO $pdo, int $productId, int $qty = 1): bool
{
    $sql = "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$qty, $productId, $qty]);
    
    return $stmt->rowCount() > 0; // 受影響行數 > 0 表示扣減成功
}

// 使用
$pdo->beginTransaction();
try {
    if (deductStock($pdo, 123, 1)) {
        // 建立訂單...
        $pdo->commit();
        return ['success' => true, 'msg' => '下單成功'];
    } else {
        $pdo->rollBack();
        return ['success' => false, 'msg' => '庫存不足'];
    }
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}

方案四:檔案鎖 flock 完整實作

function acquireLock(string $lockName, int $timeout = 10): ?resource
{
    $lockFile = sys_get_temp_dir() . "/lock_{$lockName}.lock";
    $fp = fopen($lockFile, 'c+'); // 開啟/建立,指標在開頭
    
    if (!$fp) return null;
    
    $start = microtime(true);
    while (microtime(true) - $start < $timeout) {
        // LOCK_EX 獨佔鎖 | LOCK_NB 非阻塞
        if (flock($fp, LOCK_EX | LOCK_NB)) {
            return $fp; // 取得鎖,呼叫端負責釋放
        }
        usleep(10000); // 10ms 重試
    }
    
    fclose($fp);
    return null; // 逾時
}

function processOrder(int $productId, int $userId): array
{
    $lock = acquireLock("product_{$productId}", 5);
    if (!$lock) {
        return ['success' => false, 'msg' => '系統繁忙,請稍後再試'];
    }
    
    try {
        // 這裡執行資料庫操作(已序列化)
        $pdo->beginTransaction();
        $stmt = $pdo->prepare("SELECT stock FROM products WHERE id=? FOR UPDATE");
        $stmt->execute([$productId]);
        $stock = $stmt->fetchColumn();
        
        if ($stock <= 0) {
            throw new Exception('庫存不足');
        }
        
        $pdo->exec("UPDATE products SET stock = stock - 1 WHERE id = $productId");
        // 建立訂單...
        
        $pdo->commit();
        return ['success' => true];
    } catch (Exception $e) {
        $pdo->rollBack();
        return ['success' => false, 'msg' => $e->getMessage()];
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

方案五:Redis Lua 腳本(分散式高併發首選)


// Lua 腳本:原子檢查並扣減
$luaScript = <<script('LOAD', $luaScript);

function deductStockRedis(int $productId, int $qty = 1): array
{
    global $redis, $sha;
    $key = "stock:product:$productId";
    
    // 初始化庫存(僅首次)
    $redis->setnx($key, 100); // 假設初始 100
    
    $result = $redis->evalSha($sha, 1, $key, $qty);
    
    if ($result === -1) return ['success' => false, 'msg' => '商品不存在'];
    if ($result === 0) return ['success' => false, 'msg' => '庫存不足'];
    
    return ['success' => true, 'remaining' => $result];
}

// 同步回資料庫(異步寫入)
// 可用排程任務定期將 Redis 庫存同步至 MySQL

綜合建議:分層防護架構

// 第 1 層:前端防重(按鈕置灰、Token 防重複提交)
// 第 2 層:接入層限流(Nginx limit_req、Redis 計數器限流)
// 第 3 層:業務層原子扣減(Redis Lua / 條件 UPDATE)
// 第 4 層:資料庫最終一致性(異步同步、對帳任務)

// 典型組合:
// 高併發秒殺 → Redis Lua 扣減 + 異步寫入 MySQL + 訂單隊列
// 一般電商 → 條件 UPDATE + 事務 + 適度鎖
// 簡單內部系統 → flock + 事務

避坑檢查清單

  • [ ] 資料庫隔離級別:READ COMMITTED 即可(條件 UPDATE 不需 Serializable)
  • [ ] 明確檢查 rowCount() / affected_rows 而非再次 SELECT
  • [ ] 事務範圍最小化:只包含扣減與訂單建立
  • [ ] 死鎖預防:固定順序存取多表、適當索引
  • [ ] Redis 同步策略:定時任務 / 變更通知 / 雙寫
  • [ ] 壓測驗證:模擬真實併發、監控 CPU/IO/鎖等待
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved