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

Yii + MySQL 表鎖防併發重複資料:READ/WRITE LOCK 實戰與死鎖避坑指南

     編輯:PHP 綜合知識

前言

高併發場景下(搶購、簽到、庫存扣減),單純靠應用層檢查無法防止重複資料。MySQL LOCK TABLES 提供表級鎖定,配合 Yii 命令列介面可構建簡單可靠的防併發機制。本文詳細解析讀鎖/寫鎖差異、本地讀鎖、Yii 實作範例、死鎖風險與替代方案。

MySQL 表鎖類型對比

鎖類型SQL當前連線其他連線適用場景
讀鎖 (READ)LOCK TABLES t READ可讀,不可寫可讀,不可寫報表查詢、備份、需一致性快照
寫鎖 (WRITE)LOCK TABLES t WRITE可讀可寫(獨佔)阻塞(不可讀寫)關鍵區段:檢查-插入、庫存扣減
本地讀鎖 (READ LOCAL)LOCK TABLES t READ LOCAL可讀,不可寫可讀,允許並發 INSERTMyISAM 表大量載入資料同時查詢

Yii 實作:每日單次記錄防重(寫鎖模式)

class SysRun extends CActiveRecord {
    /**
     * 取得或建立當日標記記錄(防併發重複建立)
     * @param string $marking 標記代碼
     * @param string $dayTime 日期 Y-m-d
     * @return SysRun
     */
    public function getPointAlready($marking, $dayTime) {
        // 1. 嘗試讀取(無鎖,效能最佳)
        $model = self::model()->findByAttributes(array(
            'syr_marking' => $marking,
            'syr_daytime' => $dayTime,
        ));

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

        // 2. 進入關鍵區段:加寫鎖
        Yii::app()->db->createCommand("LOCK TABLES {{sys_run}} WRITE")->execute();

        try {
            // 3. 雙重檢查(鎖內再次查詢)
            $model = self::model()->findByAttributes(array(
                'syr_marking' => $marking,
                'syr_daytime' => $dayTime,
            ));

            if ($model === null) {
                // 4. 建立新記錄
                $model = new self();
                $model->syr_marking = $marking;
                $model->syr_daytime = $dayTime;
                $model->syr_val = 0;
                $model->syr_subval = 0;
                $model->save(false); // 跳過驗證加速
            }
        } finally {
            // 5. 必須解鎖(finally 確保異常時也釋放)
            Yii::app()->db->createCommand("UNLOCK TABLES")->execute();
        }

        return $model;
    }
}

關鍵細節解析

  1. 雙重檢查鎖定模式:鎖外先查一次,鎖內再查一次,減少鎖持有時間。
  2. 表名格式:{{sys_run}} 會自動套用 tablePrefix,生產環境為 tbl_sys_run。
  3. 事務與鎖互斥:LOCK TABLES 會隱式提交當前事務,不能在事務中使用。
  4. finally 保證解鎖:PHP 5.5+ 支援 finally,舊版本需用 register_shutdown_function 或 try-catch 捕獲所有異常後解鎖。
  5. 連線持久化:CLI 長腳本需確保 DB 連線不斷線,否則鎖會隨連線中斷自動釋放。

死鎖風險與避坑指南

風險原因對策
多表鎖定順序不一致執行緒 A 鎖 t1→t2,執行緒 B 鎖 t2→t1全專案統一表鎖順序(按表名字母序)
鎖持有時間過長鎖內執行複雜邏輯、外部 API 呼叫鎖內僅執行極簡單的查詢/插入,複雜邏輯移至鎖外
連線中斷未解鎖PHP 致命錯誤、max_execution_time 超時使用 finally、設定較短 lock_wait_timeout、監控連線池
InnoDB 不支援 LOCK TABLESInnoDB 預設行鎖,表鎖需關閉 autocommit改用行鎖(SELECT ... FOR UPDATE)或 Redis 分散式鎖

更優替代方案對比

方案優點缺點適用場景
MySQL 表鎖簡單、無額外依賴粗粒度、阻塞讀、InnoDB 支援差MyISAM 表、低併發、簡單關鍵區段
行鎖 SELECT FOR UPDATE細粒度、InnoDB 原生、支援事務需索引支援、易產生間隙鎖高併發庫存扣減、訂單建立
Redis 分散式鎖 (SETNX)跨服務、高效能、可設 TTL 自動過期需 Redis 叢集、單點故障風險微服務、多語言、高併發
資料庫唯一索引最簡單、DB 保證、效能最好無法自定義錯誤處理邏輯所有防重場景首選

最佳實踐建議

  1. 首選唯一索引:在 (marking, daytime) 建立 UNIQUE KEY,捕獲 Duplicate entry 例外即可,最簡單有效。
  2. 次選行鎖:SELECT * FROM sys_run WHERE marking=? AND daytime=? FOR UPDATE 在事務中執行。
  3. 最後才用表鎖:僅在不支援行鎖的舊式 MyISAM 表或極特殊場景使用。

相關專題推薦

表鎖是防併發工具箱中的「大錘」,雖然有效但副作用大。現代應用優先使用唯一索引 + 行鎖 + Redis 分散式鎖組合策略,保留表鎖作為兼容舊系統的備選方案。本文範例展示正確的雙重檢查與 finally 解鎖模式,若必須使用請嚴格遵循。

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