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

Yii + MySQL 表鎖防止并發重復數據:LOCK TABLES WRITE 實戰與事務替代方案對比

     編輯:PHP 相關知識

Yii + MySQL 表鎖防止并發重復數據:LOCK TABLES WRITE 實戰與事務替代方案對比

高并發場景下「檢查不存在則插入」模式極易產生重復數據。本文展示使用 MySQL LOCK TABLES WRITE 在 Yii 中實現表級獨佔鎖的方案,並深度對比事務+行鎖、唯一索引、Redis 分布式鎖等替代方案優劣。

鎖類型速查

鎖類型SQL特性適用場景
讀鎖 (共享鎖)LOCK TABLES t READ所有線程可讀,不可寫備份、全表掃描報表
本地讀鎖LOCK TABLES t READ LOCAL允許并發插入 (MyISAM)MyISAM 表并發插入
寫鎖 (獨佔鎖)LOCK TABLES t WRITE僅持鎖線程可讀寫,其他阻塞關鍵區串行化、防重復插入
解鎖UNLOCK TABLES釋放當前會話所有表鎖必須成對使用

Yii 實戰代碼:每日單據狀態初始化防重

<?php
class SysRun extends CActiveRecord {
    /**
     * 獲取或創建當日標記記錄 (防并發重復)
     * @param string $marking 業務標記 (如 'daily_bonus')
     * @param string $dayTime 日期 (如 '2024-01-15')
     * @return SysRun
     */
    public function getPointAlready($marking, $dayTime) {
        // 1. 首次嘗試查找 (無鎖快速路徑)
        $model = self::model()->findByAttributes([
            'syr_marking' => $marking,
            'syr_daytime' => $dayTime,
        ]);

        if ($model !== null) {
            return $model; // 已存在,直接返回
        }

        // 2. 獲取寫鎖 (串行化檢查+插入)
        Yii::app()->db->createCommand()
            ->setText("LOCK TABLES {{sys_run}} WRITE")
            ->execute();

        try {
            // 3. 雙重檢查 (鎖內再次查詢,防止獲取鎖前已被創建)
            $model = self::model()->findByAttributes([
                'syr_marking' => $marking,
                'syr_daytime' => $dayTime,
            ]);

            if ($model === null) {
                // 4. 創建新記錄
                $model = new SysRun();
                $model->syr_marking = $marking;
                $model->syr_daytime = $dayTime;
                $model->syr_val = 0;
                $model->syr_subval = 0;
                $model->save(); // 這裡若失敗需拋出異常觸發 catch
            }
        } finally {
            // 5. 釋放鎖 (無論成功失敗必須釋放)
            Yii::app()->db->createCommand()
                ->setText("UNLOCK TABLES")
                ->execute();
        }

        return $model;
    }
}

關鍵細節深度解析

1. 雙重檢查鎖定模式

// 外層無鎖查詢 → 獲取鎖 → 內層再次查詢 → 插入
// 原因:獲取鎖期間可能已有其他進程創建記錄
// 此模式同適用於 Java Double-Checked Locking、Redis SETNX

2. try/finally 保證解鎖

  • PHP 5.5+ 支持 finally 塊,異常拋出時也會執行解鎖
  • 若 PHP < 5.5,需用 register_shutdown_function 或手動在 catch 中解鎖再拋出

3. 表名占位符 {{sys_run}}

  • Yii 會自動替換為帶前綴的真實表名 (如 zd_sys_run)
  • 原生 SQL 寫法:LOCK TABLES `zd_sys_run` WRITE

4. 死鎖風險與超時

  • LOCK TABLES 不支持超時設置,死鎖只能靠 KILL 進程
  • 嚴禁在鎖內執行耗時操作 (HTTP 請求、睡眠、大事務)
  • 建議鎖內操作 < 10ms,僅執行簡單 INSERT

方案對比:為什麼不推薦 LOCK TABLES 作為常規方案

方案原理優點缺點推薦指數
LOCK TABLES WRITE表級獨佔鎖語義直觀、兼容所有引擎并發度極低、無超時、易死鎖、阻塞所有讀寫⭐⭐
事務 + SELECT ... FOR UPDATE行級排他鎖粒度細、支持死鎖檢測、標準 SQL需索引命中、間隙鎖可能過大⭐⭐⭐⭐
唯一索引 + INSERT IGNORE / ON DUPLICATE KEY數據庫約束性能最好、無鎖、原子性強、代碼最簡需業務鍵可建唯一索引、自增 ID 浪費⭐⭐⭐⭐⭐
Redis SETNX (分布式鎖)原子設置 key跨服務、高性能、可設置 TTL 自動過期需維護 Redis、單點故障、時鐘漂移⭐⭐⭐⭐
INSERT ... ON DUPLICATE KEY UPDATEupsert 語義單條 SQL 完成檢查+插入/更新、無競態自增 ID 仍會增長、觸發器執行兩次⭐⭐⭐⭐⭐

最佳實踐:唯一索引方案 (推薦)

-- 表結構:業務鍵建立唯一索引
CREATE TABLE `sys_run` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `syr_marking` VARCHAR(50) NOT NULL,
    `syr_daytime` DATE NOT NULL,
    `syr_val` INT DEFAULT 0,
    `syr_subval` INT DEFAULT 0,
    UNIQUE KEY `uk_marking_day` (`syr_marking`, `syr_daytime`) -- 核心:防重復
) ENGINE=InnoDB;
// Yii 代碼:一條 SQL 解決
public function getPointAlready($marking, $dayTime) {
    $sql = "INSERT INTO {{sys_run}} (syr_marking, syr_daytime, syr_val, syr_subval)
            VALUES (:marking, :daytime, 0, 0)
            ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)";
    
    Yii::app()->db->createCommand($sql)->execute([
        ':marking' => $marking,
        ':daytime' => $dayTime,
    ]);
    
    $id = Yii::app()->db->getLastInsertID();
    return self::model()->findByPk($id);
}

總結

LOCK TABLES WRITE 是「核武器級」方案:威力大但副作用嚴重,**僅在無法建立唯一索引、無法使用事務行鎖、跨引擎兼容等極端場景下作為最後手段**。現代應用首選 唯一索引 + ON DUPLICATE KEY UPDATE 或 事務 + SELECT FOR UPDATE,配合 Redis 分布式鎖解決跨服務協調。理解鎖粒度、死鎖機制、雙重檢查模式,是架構高并發系統的必修課。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved