兩階段鎖 (2PL) 與寫偏斜

★★★★Reviewed

FromDDIADatabase Internals

📚 From the Books

兩階段鎖 (2PL) 與寫偏斜Transactions

2PL 用「先把鎖收齊、再一次放完」這條紀律達成 serializable。它正確、悲觀,但代價是死鎖與低吞吐——這正是現代系統轉向樂觀 SSI 的理由。

🧠 Intuition

TIP

2PL 的「兩階段」不是指交易有兩段,而是指鎖只能先一路拿、再一路放,中間不准回頭再拿。一旦放了第一把鎖就進入收縮期,之後不能再取任何鎖——這條單向紀律才是它能保證序列化的原因。

⚖️ Tradeoffs

共享鎖 vs 獨佔鎖 — 讀寫互斥的基本盤
  • 共享鎖 (S lock):讀取時取得,多個讀者可同時持有同一資源的 S 鎖。
  • 獨佔鎖 (X lock):寫入時取得,與任何其他鎖互斥;要寫就得獨佔。
  • 因此 2PL 下讀會擋寫、寫會擋讀——這正是它與 MVCC 最大的體感差異(MVCC 讀寫不互相阻塞)。
  • 讀者升級成寫者時需把 S 鎖升級為 X 鎖,這一步是常見的死鎖來源。
Predicate lock 與 index-range lock — 擋幻讀與寫偏斜
  • 光鎖「已存在的列」擋不住幻讀:別人可以插入一筆符合條件的新列。要擋住得鎖住「條件」本身。
  • Predicate lock:鎖住一個查詢條件(如「room=123 且時段重疊」),任何想插入/修改符合此條件的列都被擋住——概念正確但難高效實作。
  • Index-range lock(next-key lock):實務近似做法,鎖住索引上的一段範圍(如某索引值區間),比 predicate lock 粗但快得多,是 MySQL/InnoDB 等的實作方式。
  • 這層鎖正是 2PL 能擋住寫偏斜與幻讀、達成真正 serializable 的關鍵。
達成 serializable 的代價 — 死鎖與低吞吐
  • 死鎖:交易互相等對方的鎖形成環;資料庫得偵測(等待圖)並犧牲一個交易 abort 重試,重試成本高。
  • 吞吐低、延遲尾巴長:鎖讓衝突的交易排隊,高競爭下 throughput 直接塌;一個慢交易持鎖會拖垮一整串等它的交易。
  • 嚴格 2PL 把鎖持有到 commit,鎖持有時間長,進一步放大上述問題。
  • 結論:2PL 正確性無可挑剔,但在高並發 OLTP 下常是效能瓶頸。
對比 SSI — 悲觀 vs 樂觀
  • 2PL(悲觀):假設一定會衝突,先上鎖擋住,代價是阻塞與死鎖。
  • SSI(樂觀,可序列化快照隔離):建在 MVCC 上,讓交易先在快照上跑、不上讀鎖,commit 時才檢查是否出現危險的讀寫相依,有才 abort 重試。
  • 低競爭時 SSI 幾乎零阻塞、吞吐遠勝 2PL;高競爭時 SSI 的 abort 重試會變多,但通常仍優於鎖排隊。
  • 現代追求 serializable 的系統(如 PostgreSQL 的 Serializable 級別)多選 SSI 而非 2PL。

🔑 Takeaways

✍️ My Notes

2026-07-01

  • Q: 既然 SSI 通常更快,2PL 是不是已經沒用了?
  • A: 不是。2PL 在高競爭、衝突幾乎必然發生的工作負載下反而更穩——因為樂觀法在這種情況下會一直 abort 重試,做白工的成本可能超過直接排隊等鎖。2PL 的行為也更可預測(阻塞而非重試),對需要穩定延遲、或交易很長很貴重試代價高的場景更合適。選 2PL 還是 SSI,本質是「你預期衝突多不多」:衝突少選樂觀 (SSI),衝突多且重試貴選悲觀 (2PL)。

📖 Further Reading