場景一:單個事務更新一條存在的數據
假設有表 user (id PK, name, age)
,數據:[id=1, name='Alice', age=25]
你的 SQL: UPDATE user SET age = 26 WHERE id = 1;
底層動作:
- 事務 A (主動方) 發起更新請求。
- Lock Manager 介入:
- 查找
id=1
的索引記錄: Lock Manager 根據id=1
(主鍵)找到對應的主鍵索引樹葉子節點中的那條索引記錄。 - 檢查這條索引記錄上的鎖狀態: 發現
id=1
這條索引記錄此刻是無鎖狀態。 - 在索引記錄上“粘貼”一個鎖標記: Lock Manager 會在
id=1
這條具體的索引記錄上,打上一個**“X 鎖 (Exclusive Lock)”**的標記。- 這個標記就是一條內部的內存數據結構,記錄著:“
id=1
這條索引記錄,現在被事務 A 以X
模式鎖住,并且引用計數+1”。
- 這個標記就是一條內部的內存數據結構,記錄著:“
- 在對應的表頭部“粘貼”一個意向鎖標記: 同時,Lock Manager 還會順手在
user
表的內部元數據結構上,打上一個**“IX 鎖 (Intention Exclusive Lock)”**的標記。- 這個標記是:“
user
表的某個地方,有事務正在嘗試或已經持有X
型行鎖。”
- 這個標記是:“
- 查找
- 事務 A 執行更新: 事務 A 獲得鎖,可以安全地修改
id=1
這條索引記錄的age
值。 - 事務 A 提交/回滾: 事務 A 結束時,Lock Manager 會根據之前的記錄,移除
id=1
上的X
鎖標記,同時檢查user
表是否還有其他IX
鎖持有者,如果沒有,也移除user
表上的IX
鎖標記。
場景二:事務 A 更新數據,事務 B 隨后讀取同一條數據
數據:[id=1, name='Alice', age=25]
你的 SQL (事務 A): UPDATE user SET name = 'Alicia' WHERE id = 1;
你的 SQL (事務 B): SELECT * FROM user WHERE id = 1;
底層動作:
- 事務 A 獲得
id=1
的X
鎖 (如場景一所述)。id=1
索引記錄上:X
鎖,持有者事務 A
。user
表元數據上:IX
鎖,持有者事務 A
。
- 事務 B 發起讀取請求。
- Lock Manager 介入:
- 查找
id=1
的索引記錄。 - 檢查這條索引記錄上的鎖狀態: 發現
id=1
這條索引記錄上有一個X
鎖,并且持有者是**事務 A
**。 - 判斷沖突: 事務 B 嘗試讀取,但
事務 A
持有的是X
鎖 (排他鎖
)。X
鎖會阻止所有其他事務的讀寫。 - 不授予鎖,并等待: Lock Manager 不授予事務 B 任何鎖,而是把事務 B 放入一個等待隊列,同時啟動事務 B 的等待計時器。
- “事務 B 正在等待
id=1
這條索引記錄上的鎖。”
- “事務 B 正在等待
- 查找
- 事務 A 提交: 釋放
id=1
上的X
鎖,也釋放user
表的IX
鎖。 - Lock Manager 通知:
id=1
上的鎖被移除,Lock Manager 發現等待隊列中有事務 B。 - 事務 B 被喚醒: 事務 B 獲得執行權限,讀取
id=1
這條記錄的新數據(比如name='Alicia'
)。
場景三:間隙鎖 (Gap Lock) 的具體行為 (防止幻讀)
數據:user (id PK)
,記錄只有 [id=10], [id=30]
(沒有 id=20
)
你的 SQL (事務 A): SELECT * FROM user WHERE id BETWEEN 15 AND 25 FOR UPDATE;
(注意這是范圍查詢且帶 FOR UPDATE
)
底層動作:
- 事務 A 發起請求。
- Lock Manager 介入:
- 查找索引: Lock Manager 根據條件
id BETWEEN 15 AND 25
,在主鍵索引樹上進行查找。 - 發現沒有符合條件的記錄 (這是一個空區間/間隙)。
- 在“間隙”上打鎖標記: 盡管沒有找到具體的數據行,Lock Manager 依然會在索引結構中,針對
id=10
和id=30
之間的**“范圍”(即(10, 30)
這個間隙),打上一個“間隙鎖 (Gap Lock)”**的標記。- 這個標記就是:“索引中
id
值在10
和30
之間的空地,現在被事務 A 鎖住,禁止插入新數據。” - 通常,這個間隙鎖是
X
類型的,因為它阻止其他事務在這個間隙中進行INSERT
操作。
- 這個標記就是:“索引中
- 在對應的表頭部“粘貼”一個意向鎖標記 (IX)。
- 查找索引: Lock Manager 根據條件
- 事務 B 嘗試插入數據:
INSERT INTO user (id) VALUES (20);
- Lock Manager 介入:
- 判斷插入位置: 發現
id=20
應該插入到id=10
和id=30
之間。 - 檢查該間隙的鎖狀態: 發現這個
(10, 30)
的間隙上有一個間隙鎖,持有者是**事務 A
**。 - 不授予鎖,并等待: Lock Manager 不授予事務 B 任何鎖,將事務 B 放入等待隊列。
- 判斷插入位置: 發現
- 事務 A 提交: 釋放
(10, 30)
上的間隙鎖,以及user
表的IX
鎖。 - Lock Manager 通知: 間隙鎖被移除,事務 B 被喚醒,可以成功插入
id=20
。
這些“鎖標記”本質上都是數據庫系統內部維護的內存數據結構,它們記錄著:哪個事務在哪個資源(索引記錄或間隙或表)上持有哪種類型的鎖。當其他事務請求時,Lock Manager 就去查這些標記,進行兼容性判斷,決定是立即授予、等待還是死鎖。
內存中的鎖管理數據結構,它們并不是簡單的“標記”那么純粹,而是一系列精巧組織的對象。
要理解這個,我們得從 Lock Manager (鎖管理器) 的核心工作開始。Lock Manager 維護著一張“活的地圖”,這張地圖記錄了哪些資源被鎖了,被誰鎖了,鎖的類型是什么,以及誰在等待這些鎖。
最底層數據結構模擬:Lock Manager 的“活地圖”
想象 Lock Manager 就好比一個大型交通控制中心,它有幾塊巨大的顯示屏和一些重要的記錄本。
核心數據結構 1:鎖哈希表 (Lock Hash Table) 或 鎖鏈表 (Lock List)
這是所有正在活動的鎖及其等待者的“索引”。
- 目的:快速查找某個資源(比如某行數據)上是否有鎖,以及有哪些事務在等待。
- 實現:通常是一個哈希表(
std::unordered_map
類似),鍵是資源標識符,值是一個鏈表或隊列,里面包含了所有作用在該資源上的鎖對象和等待者。因為哈希表的查找速度快,能迅速定位到某個資源。
模擬其內部結構:
// 這是一個高度簡化的偽代碼,模擬內存中的核心結構// 1. 資源標識符 (Resource Identifier) - 鎖住哪個具體的“東西”
// 這是鎖的“粒度”所在,可以是一個Page ID + Index ID + Record ID,也可以是表ID
struct LockResource {enum ResourceType {TABLE_LOCK, // 表級RECORD_LOCK, // 行級GAP_LOCK // 間隙};ResourceType type;long long tableId; // 表的唯一標識long long pageId; // 數據頁的唯一標識 (行鎖和間隙鎖可能需要)long long indexId; // 索引的唯一標識 (行鎖和間隙鎖需要)// 對于Record Lock,可能還需要存儲記錄的在頁面內的具體位置或哈希值// 對于Gap Lock,可能需要存儲間隙的起始和結束點(如索引鍵值,或其他內部指針)std::string recordKeyHash; // 簡化表示:實際是索引鍵值的hash或物理位置// 確保 LockResource 可以作為哈希表的鍵bool operator==(const LockResource& other) const { /* 比較所有成員 */ }size_t operator()(const LockResource& res) const { /* 計算哈希值 */ }
};// 2. 具體的鎖對象 (Lock Object) - 鎖本身的信息
struct LockObject {enum LockMode {IS_LOCK, // Intention Shared (表級意向共享)IX_LOCK, // Intention Exclusive (表級意向排他)S_LOCK, // Shared (讀鎖,共享鎖)X_LOCK // Exclusive (寫鎖,排他鎖)};LockMode mode;long long transactionId; // 持有這個鎖的事務IDint lockCount; // 鎖計數 (用于可重入性), 比如 SELECT ...FOR UPDATE 兩次bool isWaiting; // 這個事務是否正在等待這個鎖?// 指向下一個等待這個資源的鎖對象(如果存在的話)// 或者指向下一個被該事務持有的鎖對象LockObject* nextLockInResourceList; // 針對同一資源的所有鎖和等待者鏈表LockObject* nextLockInTxList; // 某個事務持有的所有鎖鏈表
};// 3. 鎖哈希表 - 核心的數據結構
// Key: LockResource (哪個資源被鎖)
// Value: 一個鏈表/隊列,包含所有作用在該資源上的 LockObject
std::unordered_map<LockResource, std::list<LockObject>> globalLockHashTable;
理解 globalLockHashTable
里的“東西”:
- 每個節點上:
- 沒有獨立的“鎖標記”。相反,數據庫管理著一個集中的 Lock Manager。
- 當你說的“節點”是表時,表上會有意向鎖(
IS
或IX
)的記錄,這些記錄也會被存放在globalLockHashTable
中。LockResource
的type
會是TABLE_LOCK
。 - 當你說的“節點”是行時,它指的就是索引記錄 (Index Record)。這才是 InnoDB 行級鎖的真正目標。
LockResource
的type
會是RECORD_LOCK
或GAP_LOCK
。
核心數據結構 2:事務持有的鎖列表 (Transaction’s Lock List)
除了按資源查找鎖,Lock Manager 還需要知道一個事務到底持有哪些鎖,以便在事務提交或回滾時能迅速釋放它們。
- 目的:快速釋放一個事務持有的所有鎖。
- 實現:每個活躍事務內部,或者 Lock Manager 維護一個映射:
Transaction ID -> List of LockObject
。
模擬其內部結構:
// 4. Per-Transaction Lock List - 每個活躍事務會有一個這樣的內部列表
// 一個事務 A 內部可能有一個指針指向它所持有的第一個 LockObject
// 或者 Lock Manager 維護一個 map:
std::unordered_map<long long, std::list<LockObject*>> transactionLocksMap;
// 這個 list 里面的 LockObject* 都是上面 globalLockHashTable 里的指針
可視化模擬:
假設有表 user (id PK, name)
,數據:[id=1], [id=5], [id=10]
事務 A
操作:UPDATE user SET name='New' WHERE id=1;
事務 B
操作:SELECT * FROM user WHERE id BETWEEN 3 AND 7 FOR UPDATE;
Lock Manager 內部狀態(簡化):
{"globalLockHashTable": {// 資源1: 用戶表, TABLE_LOCK類型"Resource_Table_user": [{"mode": "IX_LOCK", // 意向排他鎖"transactionId": "TxA","lockCount": 1,"isWaiting": false},{"mode": "IX_LOCK", // 意向排他鎖 (TxB也會加IX)"transactionId": "TxB","lockCount": 1,"isWaiting": false}],// 資源2: id=1 的索引記錄, RECORD_LOCK類型"Resource_Record_user_id_1": [{"mode": "X_LOCK", // 排他鎖"transactionId": "TxA","lockCount": 1,"isWaiting": false}],// 資源3: "(1,5)" 間隙(id=5前面),GAP_LOCK類型"Resource_Gap_user_(1,5)": [{"mode": "X_LOCK", // 間隙鎖是排他的"transactionId": "TxB","lockCount": 1,"isWaiting": false}],// 資源4: "id=5" 記錄,RECORD_LOCK類型"Resource_Record_user_id_5": [{"mode": "X_LOCK", // Next-key lock會包含記錄本身"transactionId": "TxB","lockCount": 1,"isWaiting": false}],// 資源5: "(5,10)" 間隙,GAP_LOCK類型"Resource_Gap_user_(5,10)": [{"mode": "X_LOCK", // 間隙鎖是排他的"transactionId": "TxB","lockCount": 1,"isWaiting": false}]// ... 其他資源},"transactionLocksMap": {"TxA": ["Resource_Table_user[IX_LOCK_TxA]","Resource_Record_user_id_1[X_LOCK_TxA]"],"TxB": ["Resource_Table_user[IX_LOCK_TxB]","Resource_Gap_user_(1,5)[X_LOCK_TxB]","Resource_Record_user_id_5[X_LOCK_TxB]","Resource_Gap_user_(5,10)[X_LOCK_TxB]"]}
}
死鎖檢測器的“行為”:
死鎖檢測器會定期(或在每次等待發生時)遍歷 globalLockHashTable
中的等待鏈表,并結合 transactionLocksMap
來構建一個**“等待圖 (Waits-for Graph)”**。
等待圖:
- 節點:事務 ID (TxA, TxB)。
- 邊:如果 TxA 在等待 TxB 釋放某個鎖,則從 TxA 指向 TxB。
偽算法:
- “老鐵,數據庫里現在誰在等誰啊?”
- 遍歷
globalLockHashTable
里的每一個LockObject
。 - 如果
LockObject.isWaiting
是true
:- 找出這個
LockObject
對應的LockResource
。 - 找出目前正在持有這個
LockResource
上的沖突鎖的那個LockObject
的transactionId
(假設是TxC
)。 - 那么,
LockObject.transactionId
正在等待TxC
。 - 在內存的**“等待圖”**中,就畫一條邊:
LockObject.transactionId
-->TxC
。
- 找出這個
- “圖畫好了!現在我們看看有沒有循環”
- 在“等待圖”中進行深度優先搜索 (DFS) 或拓撲排序等算法來檢測是否存在環。
- 如果發現
TxA --> TxB --> TxC --> TxA
這樣的循環,警報!死鎖!
- 如果發現
- “有了循環!挑選一個受害者,把它回滾,讓它釋放所有鎖,打破這個循環!”
“每個節點上每個表上都有鎖標記嗎?”
- 不是每個表“節點”上都有獨立的鎖標記,而是統一由
Lock Manager
在內存中管理這些LockObject
實例。 - 表上:會有
IS/IX
意向鎖的LockObject
。 - 行上:特指索引記錄 (Index Record) 上,會有
S/X
共享/排他鎖的LockObject
。 - 間隙上:特指索引的空閑區域 (Gap) 上,會有
Gap Lock
的LockObject
。
所有這些 LockObject
都被組織在 globalLockHashTable
中(按資源分類)以及 transactionLocksMap
中(按事務分類),供 Lock Manager 高效地查找、管理、沖突檢測和死鎖檢測。它們是實時變化的內存數據,支撐著數據庫的并發控制。