區分 lock(交易層級、邏輯物件)與 latch(執行緒層級、記憶體結構),以及 PostgreSQL 的三層鎖:heavyweight lock、LWLock、spinlock。
寫出 S/X/IS/IX/SIX 的相容矩陣,解釋多粒度鎖與意向鎖的目的。
證明兩階段鎖(2PL)保證衝突可序列化,並說明為何實務使用 strict 2PL。
以等待圖偵測死結、以 wait-die/wound-wait 預防死結,並知道 PostgreSQL 的 機制。
熟記 PostgreSQL 的 8 種表級鎖與 4 種列級鎖的衝突關係,避免 DDL 鎖住整個系統的事故。
用 、 診斷鎖等待。
Lock 與 Latch
lP5.2cmP5.2cm & Lock(交易鎖) & Latch(閂鎖)
保護對象 & 邏輯物件:表、列、索引鍵範圍、述詞 & 記憶體結構:buffer frame、雜湊表桶、B+Tree 頁
持有者 & 交易 & 執行緒/程序,一小段程式碼
持有時間 & 到交易結束(2PL) & 關鍵區段(微秒)
模式 & 多種(S、X、意向鎖……) & 通常只有 read/write
死結 & 偵測或預防 & 以編碼紀律避免(固定順序、不等待)
PostgreSQL 名稱 & heavyweight lock( 可見) & LWLock、spinlock、buffer pin
第 3、4 章的 latch crabbing、buffer header 鎖都屬於 latch;本章的主角是交易鎖。
鎖模式與多粒度鎖
最基本的兩種模式:共享鎖 S(讀)與排他鎖 X(寫)。若鎖的粒度是整張表,並行度太低;若只有列鎖, 一個全表掃描要取百萬個鎖,鎖表爆炸。Gray 等人(1976)提出多粒度鎖與意向鎖(intention lock): 在上層節點(資料庫 \(\to\) 表 \(\to\) 頁 \(\to\) 列)宣告「我打算在下層取某種鎖」。
| 已持有 \(\backslash\) 請求 | IS | IX | S | SIX | X |
|---|---|---|---|---|---|
| IS | |||||
| IX | |||||
| S | |||||
| SIX | |||||
| X |
IS / IX:打算在下層取 S / X。要鎖某列 X 之前,先在表上取 IX、在頁上取 IX。
SIX = S + IX:讀整張表,同時更新其中少數列。
全表掃描在表上取 S,就與任何 IX(有人在改某列)衝突——正確地阻止了讀到半更新狀態;而兩個 IX 相容,改不同列的交易互不干擾(列層級再判斷)。
鎖升級(escalation):列鎖太多時換成表鎖以省資源(SQL Server、DB2 會;PostgreSQL 不會,因為列鎖存在 tuple 裡不佔鎖表)。
兩階段鎖
兩階段鎖(2PL):每筆交易分為成長期(只取鎖、不放鎖)與縮減期(只放鎖、不取鎖)。 一旦釋放任何鎖,就不能再取得新鎖。
定理:2PL 產生的排程都是衝突可序列化的。證明概要:令每筆交易的 lock point 為它取得最後一個鎖的時刻; 若 \(T_i \to T_j\) 有衝突邊,則 \(T_i\) 必須先釋放該資料的鎖、\(T_j\) 才能取得,故 \(\text{lockpoint}(T_i) < \text{lockpoint}(T_j)\)。 若優先圖有環,會推出 lock point 的嚴格遞增環,矛盾。
但基本 2PL 允許在縮減期釋放 X 鎖後被別人讀到尚未 commit 的值 \(\Rightarrow\) 連鎖中止。 Strict 2PL:X 鎖持有到 commit/abort;Rigorous(strong strict)2PL:所有鎖持有到結束。 實務系統都用 strict/rigorous 2PL,代價是並行度降低與死結。
死結
偵測
維護等待圖(wait-for graph):\(T_i \to T_j\) 表示 \(T_i\) 在等 \(T_j\) 持有的鎖。有環即死結,選一個受害者中止 (依據:已完成的工作量、持有的鎖數、重啟次數避免餓死)。
PostgreSQL 不常態性地檢查等待圖:一個 backend 等鎖超過 (預設 1 s)後,才由它自己跑一次等待圖檢查 ();若發現環,發現者自己成為受害者,收到 (SQLSTATE ), 訊息會列出涉及的程序與各自等待的鎖。設 可把等待超過 的事件(即使不是死結)寫進日誌。
預防
以交易時間戳(越老優先權越高)決定誰等、誰死,保證等待圖無環:
Wait-Die(非搶占):老交易等年輕交易;年輕交易要等老交易時直接中止(die)。
Wound-Wait(搶占):老交易要鎖時直接把年輕持有者中止(wound);年輕交易等老交易。
中止的交易重啟時保留原時間戳,否則會餓死。分散式系統(如早期 Spanner、CockroachDB 的某些路徑)偏好預防,因為跨節點建等待圖代價高。
固定的存取順序:多筆更新依主鍵排序後再執行( 不保證順序——用 先鎖)。
交易越短越好;把使用者互動、外部 API 呼叫移出交易。
設 讓等待快速失敗並重試,而不是堆積。
外鍵:更新父表主鍵或刪父列會對子表取鎖;子表的外鍵欄位沒索引時會全表掃描並長時間持鎖。
Phantom 與述詞鎖
Strict 2PL 鎖住的是已存在的列。\(T_1\) 執行 鎖了符合的 10 列, \(T_2\) 插入一列 不會與任何鎖衝突——phantom 出現。解法:
述詞鎖(predicate lock):鎖住條件 本身,與任何可能符合的插入衝突;判定述詞相交代價高,少有系統直接實作。
索引範圍鎖/gap lock:鎖住索引上 的區間。InnoDB 的 next-key lock(記錄鎖 + 前方的 gap 鎖)就是這個, 這也是 InnoDB 的 RR 沒有 phantom(對鎖定讀而言)的原因,但也帶來不少「為什麼插入會被鎖住」的困惑。
SSI 的 SIREAD lock:PostgreSQL 的做法——不阻塞,只記錄讀過的範圍(tuple/頁/表),事後偵測衝突(第 9 章)。
PostgreSQL 的表級鎖
PostgreSQL 有 8 種表級鎖模式,名稱源自歷史,不要望文生義( 是表級鎖,表示「我要改列」)。 每個 SQL 命令自動取得對應模式,衝突時排隊等待:
| 已持有 \(\backslash\) 請求 | AS | RS | RX | SUX | S | SRX | X | AX |
|---|---|---|---|---|---|---|---|---|
| ACCESS SHARE (AS) | ||||||||
| ROW SHARE (RS) | ||||||||
| ROW EXCLUSIVE (RX) | ||||||||
| SHARE UPDATE EXCLUSIVE (SUX) | ||||||||
| SHARE (S) | ||||||||
| SHARE ROW EXCLUSIVE (SRX) | ||||||||
| EXCLUSIVE (X) | ||||||||
| ACCESS EXCLUSIVE (AX) |
lP9.4cm 模式 & 誰取得它
ACCESS SHARE &
ROW SHARE &
ROW EXCLUSIVE & 、、、
SHARE UPDATE EXCLUSIVE & (非 FULL)、、、、多數
SHARE & (非 CONCURRENTLY)——阻擋寫入但允許讀
SHARE ROW EXCLUSIVE & 、部分 (如 )
EXCLUSIVE &
ACCESS EXCLUSIVE & 、、、、、多數 (加欄、改型別、加 NOT NULL……)、 預設
一個報表查詢在表 上持有 ACCESS SHARE 已跑了 10 分鐘。工程師執行 , 它需要 ACCESS EXCLUSIVE,於是排隊等待。問題是:鎖的等待是先進先出的——之後所有新的 (只需要 ACCESS SHARE) 都排在 ALTER 後面,因為它們與排隊中的 ACCESS EXCLUSIVE 衝突。10 秒內連線池耗盡,整個服務停擺。 對策:遷移腳本一律先 ,拿不到鎖就失敗重試; 用 、 再 、PostgreSQL 11+ 的 (常數預設不重寫表)等低鎖手段。
PostgreSQL 的列級鎖
| 已持有 \(\backslash\) 請求 | KEY SHARE | SHARE | NO KEY UPDATE | UPDATE |
|---|---|---|---|---|
| FOR KEY SHARE | ||||
| FOR SHARE | ||||
| FOR NO KEY UPDATE | ||||
| FOR UPDATE |
不改主鍵/唯一鍵欄位時取 FOR NO KEY UPDATE;改鍵或 取 FOR UPDATE。
外鍵檢查對被參照的父列取 FOR KEY SHARE——所以更新父列的非鍵欄位(NO KEY UPDATE)不會與子表的插入互相阻塞。這是 9.3 引入這兩種弱鎖的原因。
列鎖記錄在 tuple 的 (多個持有者時用 MultiXact),不佔鎖表,所以鎖百萬列也不會耗盡 ,但會產生 WAL 與髒頁。
:拿不到立即失敗;:跳過被鎖的列——實作工作佇列的標準寫法。
WITH next_job AS (
SELECT id FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE jobs SET status = 'running', worker = :me
FROM next_job WHERE jobs.id = next_job.id
RETURNING jobs.*;
Advisory lock(、)讓應用程式借用資料庫的鎖管理器協調任意資源(如「同一時間只跑一個排程器」)。
診斷鎖等待
SELECT a.pid, a.state, a.wait_event_type, a.wait_event,
pg_blocking_pids(a.pid) AS blocked_by,
now() - a.xact_start AS xact_age,
left(a.query, 60) AS query
FROM pg_stat_activity a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0 OR a.pid IN (SELECT unnest(pg_blocking_pids(pid)) FROM pg_stat_activity)
ORDER BY a.xact_start;
列出所有鎖( 為等待中); 表示在等交易鎖, 則是內部閂鎖爭用(如 、、)——那是另一類問題(I/O 或 CPU 瓶頸),不是應用程式的鎖。 找到源頭後可 (取消查詢)或 (斷線)。
樂觀並行控制
Kung 與 Robinson(1981)的 OCC:交易在私有工作區讀寫(讀取階段),commit 時驗證讀集沒有被已 commit 的並行交易改過 (backward validation:與所有在我開始後 commit 的交易比對),通過才把寫集寫入(寫入階段,通常在一個短暫的全域臨界區內)。 衝突罕見時沒有任何鎖開銷;衝突多時重做成本高,且長交易容易一直被年輕交易「驗證失敗」。 記憶體資料庫(Hekaton、Silo、TicToc)與 FoundationDB 採用 OCC 變體。對照:MVCC + first-updater-wins 其實也是一種「對寫入樂觀」的策略。
v4 實作 :、、, 支援 IS/IX/S/SIX/X 與本章的相容矩陣,strict 2PL(鎖到 commit/abort 才釋放),背景執行緒每 50 ms 建等待圖找環並中止最年輕的交易。 測試:兩個執行緒交叉更新兩列必須有一方收到 deadlock 錯誤且另一方成功;壓測中不得有鎖洩漏。 這正是 CMU 15-445 Project #4 的核心。
交易 \(T_1\) 在表 \(R\) 上取 SIX,\(T_2\) 想在 \(R\) 的某列取 X:\(T_2\) 需要先在 \(R\) 上取什麼鎖?能否取得?若 \(T_2\) 只想讀某列呢?
給出一個遵守 2PL 但發生連鎖中止的排程,再說明 strict 2PL 如何避免。
三筆交易 \(T_1(ts{=}10)\)、\(T_2(ts{=}20)\)、\(T_3(ts{=}30)\),\(T_1\) 等 \(T_3\)、\(T_3\) 等 \(T_2\)、\(T_2\) 想要 \(T_1\) 持有的鎖。在 wait-die 與 wound-wait 下分別會發生什麼?
為什麼 要掃描表兩次並等待所有既有交易結束?它取的 SHARE UPDATE EXCLUSIVE 與哪些操作衝突?
設計一個遷移腳本把 加上 NOT NULL 與唯一約束,要求全程不阻塞讀寫超過 1 秒。列出每一步取得的鎖。
用 的佇列在消費者當機時工作會不會遺失?與「先 UPDATE 標記再處理」相比,兩者的失敗模式有何不同?
(動手)開三個 重現「DDL 排隊導致 SELECT 全部阻塞」,用本章的診斷查詢觀察,然後加入 再試一次。
PostgreSQL 文件 §13.3 Explicit Locking(表級/列級/頁級鎖、死結、advisory lock): https://www.postgresql.org/docs/current/explicit-locking.html
PostgreSQL 原始碼 (鎖管理器設計)、(列鎖與 MultiXact)、。
J. Gray, R. Lorie, G. Putzolu, I. Traiger, , 1976。
H. T. Kung, J. T. Robinson, , ACM TODS 1981。
CMU 15-445 Fall 2025 Lecture #17 Two-Phase Locking Concurrency Control;Project #4 Concurrency Control。
M. Slot (Citus), PostgreSQL rocks, except when it blocks: Understanding locks: https://www.citusdata.com/blog/2018/02/15/when-postgresql-blocks/;Seven tips for dealing with Postgres locks: https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/
H. Ringhus, pglocks.org(每個 SQL 命令取得哪些鎖的互動查詢表):https://pglocks.org/
MySQL 文件 InnoDB Locking(record / gap / next-key / insert intention lock): https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html