前言
高併發場景下(搶購、簽到、庫存扣減),單純靠應用層檢查無法防止重複資料。MySQL LOCK TABLES 提供表級鎖定,配合 Yii 命令列介面可構建簡單可靠的防併發機制。本文詳細解析讀鎖/寫鎖差異、本地讀鎖、Yii 實作範例、死鎖風險與替代方案。
MySQL 表鎖類型對比
| 鎖類型 | SQL | 當前連線 | 其他連線 | 適用場景 |
| 讀鎖 (READ) | LOCK TABLES t READ | 可讀,不可寫 | 可讀,不可寫 | 報表查詢、備份、需一致性快照 |
| 寫鎖 (WRITE) | LOCK TABLES t WRITE | 可讀可寫(獨佔) | 阻塞(不可讀寫) | 關鍵區段:檢查-插入、庫存扣減 |
| 本地讀鎖 (READ LOCAL) | LOCK TABLES t READ LOCAL | 可讀,不可寫 | 可讀,允許並發 INSERT | MyISAM 表大量載入資料同時查詢 |
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;
}
}
關鍵細節解析
- 雙重檢查鎖定模式:鎖外先查一次,鎖內再查一次,減少鎖持有時間。
- 表名格式:
{{sys_run}} 會自動套用 tablePrefix,生產環境為 tbl_sys_run。
- 事務與鎖互斥:
LOCK TABLES 會隱式提交當前事務,不能在事務中使用。
- finally 保證解鎖:PHP 5.5+ 支援
finally,舊版本需用 register_shutdown_function 或 try-catch 捕獲所有異常後解鎖。
- 連線持久化:CLI 長腳本需確保 DB 連線不斷線,否則鎖會隨連線中斷自動釋放。
死鎖風險與避坑指南
| 風險 | 原因 | 對策 |
| 多表鎖定順序不一致 | 執行緒 A 鎖 t1→t2,執行緒 B 鎖 t2→t1 | 全專案統一表鎖順序(按表名字母序) |
| 鎖持有時間過長 | 鎖內執行複雜邏輯、外部 API 呼叫 | 鎖內僅執行極簡單的查詢/插入,複雜邏輯移至鎖外 |
| 連線中斷未解鎖 | PHP 致命錯誤、max_execution_time 超時 | 使用 finally、設定較短 lock_wait_timeout、監控連線池 |
| InnoDB 不支援 LOCK TABLES | InnoDB 預設行鎖,表鎖需關閉 autocommit | 改用行鎖(SELECT ... FOR UPDATE)或 Redis 分散式鎖 |
更優替代方案對比
| 方案 | 優點 | 缺點 | 適用場景 |
| MySQL 表鎖 | 簡單、無額外依賴 | 粗粒度、阻塞讀、InnoDB 支援差 | MyISAM 表、低併發、簡單關鍵區段 |
| 行鎖 SELECT FOR UPDATE | 細粒度、InnoDB 原生、支援事務 | 需索引支援、易產生間隙鎖 | 高併發庫存扣減、訂單建立 |
| Redis 分散式鎖 (SETNX) | 跨服務、高效能、可設 TTL 自動過期 | 需 Redis 叢集、單點故障風險 | 微服務、多語言、高併發 |
| 資料庫唯一索引 | 最簡單、DB 保證、效能最好 | 無法自定義錯誤處理邏輯 | 所有防重場景首選 |
最佳實踐建議
- 首選唯一索引:在
(marking, daytime) 建立 UNIQUE KEY,捕獲 Duplicate entry 例外即可,最簡單有效。
- 次選行鎖:
SELECT * FROM sys_run WHERE marking=? AND daytime=? FOR UPDATE 在事務中執行。
- 最後才用表鎖:僅在不支援行鎖的舊式 MyISAM 表或極特殊場景使用。
相關專題推薦
表鎖是防併發工具箱中的「大錘」,雖然有效但副作用大。現代應用優先使用唯一索引 + 行鎖 + Redis 分散式鎖組合策略,保留表鎖作為兼容舊系統的備選方案。本文範例展示正確的雙重檢查與 finally 解鎖模式,若必須使用請嚴格遵循。