高并發場景下「檢查不存在則插入」模式極易產生重復數據。本文展示使用 MySQL LOCK TABLES WRITE 在 Yii 中實現表級獨佔鎖的方案,並深度對比事務+行鎖、唯一索引、Redis 分布式鎖等替代方案優劣。
| 鎖類型 | SQL | 特性 | 適用場景 |
|---|---|---|---|
| 讀鎖 (共享鎖) | LOCK TABLES t READ | 所有線程可讀,不可寫 | 備份、全表掃描報表 |
| 本地讀鎖 | LOCK TABLES t READ LOCAL | 允許并發插入 (MyISAM) | MyISAM 表并發插入 |
| 寫鎖 (獨佔鎖) | LOCK TABLES t WRITE | 僅持鎖線程可讀寫,其他阻塞 | 關鍵區串行化、防重復插入 |
| 解鎖 | UNLOCK TABLES | 釋放當前會話所有表鎖 | 必須成對使用 |
<?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;
}
}
// 外層無鎖查詢 → 獲取鎖 → 內層再次查詢 → 插入 // 原因:獲取鎖期間可能已有其他進程創建記錄 // 此模式同適用於 Java Double-Checked Locking、Redis SETNX
finally 塊,異常拋出時也會執行解鎖register_shutdown_function 或手動在 catch 中解鎖再拋出zd_sys_run)LOCK TABLES `zd_sys_run` WRITELOCK TABLES 不支持超時設置,死鎖只能靠 KILL 進程| 方案 | 原理 | 優點 | 缺點 | 推薦指數 |
|---|---|---|---|---|
| LOCK TABLES WRITE | 表級獨佔鎖 | 語義直觀、兼容所有引擎 | 并發度極低、無超時、易死鎖、阻塞所有讀寫 | ⭐⭐ |
| 事務 + SELECT ... FOR UPDATE | 行級排他鎖 | 粒度細、支持死鎖檢測、標準 SQL | 需索引命中、間隙鎖可能過大 | ⭐⭐⭐⭐ |
| 唯一索引 + INSERT IGNORE / ON DUPLICATE KEY | 數據庫約束 | 性能最好、無鎖、原子性強、代碼最簡 | 需業務鍵可建唯一索引、自增 ID 浪費 | ⭐⭐⭐⭐⭐ |
| Redis SETNX (分布式鎖) | 原子設置 key | 跨服務、高性能、可設置 TTL 自動過期 | 需維護 Redis、單點故障、時鐘漂移 | ⭐⭐⭐⭐ |
| INSERT ... ON DUPLICATE KEY UPDATE | upsert 語義 | 單條 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 分布式鎖解決跨服務協調。理解鎖粒度、死鎖機制、雙重檢查模式,是架構高并發系統的必修課。