資料庫為了滿足 ACID 中的一致性與隔離性,核心手段是加鎖。但過度加鎖會扼殺高併發效能。InnoDB 的鎖設計正是在「資料正確性」與「併發效能」間尋找平衡的藝術。本文通過剖析 InnoDB 加鎖機制,幫助你理解事務處理的底層邏輯。
應用層常用「一次封鎖法」預防死鎖,但資料庫在事務開始時無法預知會訪問哪些資料,因此採用兩段鎖協議(Two-Phase Locking, 2PL):
雖無法完全避免死鎖,但 2PL 保證了併發調度的可串行化,這對資料恢復與備份至關重要。
| 隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed (RC) | 不可能 | 可能 | 可能 |
| Repeatable Read (RR, InnoDB 預設) | 不可能 | 不可能 | 可能(標準),但 MySQL 解決了 |
| Serializable | 不可能 | 不可能 | 不可能 |
Read Uncommitted 不加鎖,生產極少用,不再贅述。
核心結論:RC 下讀不加鎖,寫(INSERT/UPDATE/DELETE)加行鎖。
-- 表結構:teacher_id 有索引,class_name 無索引 UPDATE class_teacher SET class_name='初三二班' WHERE teacher_id=1; -- 僅鎖定 teacher_id=1 的行 UPDATE class_teacher SET teacher_id=3 WHERE class_name='初三一班'; -- 鎖定全表所有行!
原理:無索引時,存儲引擎無法快速過濾,只能全表加鎖後由 Server 層過濾。MySQL 為了效率,在 Server 過濾不滿足條件的行後會調用 unlock_row 釋放鎖(違背 2PL 但提升效能,參見《高性能 MySQL》P181)。
警示:大表批量更新若走不了索引,會導致大量無關行被短暫鎖定,嚴重阻塞併發。
InnoDB 預設級別,通過 MVCC(多版本並發控制) 實現「快照讀」的一致性,通過 Next-Key Lock(行鎖 + 間隙鎖) 解決「當前讀」的幻讀問題。
SELECT,讀取歷史版本,**不加鎖**,實現可重複讀。SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、INSERT/UPDATE/DELETE,讀取最新版本,**需加鎖**。事務隔離級別實際定義的是當前讀的隔離程度。MySQL 引入快照讀降低鎖競爭,提升併發。
每行隱藏兩列:創建版本號、刪除版本號(實為事務 ID)。
SELECT:讀取創建版本 ≤ 當前事務版本,且刪除版本為空或 > 當前版本的行。INSERT:保存當前事務版本為創建版本。DELETE:保存當前事務版本為刪除版本。UPDATE:插入新行(創建版本=當前版本),舊行刪除版本=當前版本。優勢:大多數讀操作無鎖,效能高;只鎖定必要行。
間隙鎖鎖定索引記錄之間的「間隙」,防止其他事務插入新記錄。Next-Key Lock = 行鎖 + 間隙鎖。
以 teacher_id 索引為例,B+ 樹索引有序,InnoDB 將其劃分區間:(-∞, 5], (5, 30], (30, +∞)。執行 UPDATE ... WHERE teacher_id=30 時,不僅行鎖鎖住 30,還在 (5,30] 和 (30,+∞) 加間隙鎖,阻塞插入。
副作用:更新不存在的值(如 teacher_id=20)仍會鎖住所在區間 (5,30];無索引列更新會鎖全表間隙,且無法像行鎖那樣由 Server 過濾後自動釋放。
讀加共享鎖,寫加排他鎖,讀寫互斥。實現簡單、資料最安全,但併發能力極差。僅適用於極低併發、極高一致性要求場景。
冷知識:別以為 SELECT 永不加鎖,在 Serializable 級別它會加共享鎖!