多版本並發控制 (MVCC)

★★★★★Reviewed

FromDDIAHigh

📚 From the Books

多版本並發控制 (MVCC)Transactions

MVCC 用「每次寫都產生新版本、讀只看自己開始時的快照」換來讀不擋寫、寫不擋讀。它是 Snapshot Isolation 的底層引擎,代價是要持續回收沒人看得到的舊版本。

🧠 Intuition

TIP

不要把資料庫想成「一格一格會被覆蓋的記憶體」,要想成「append-only 的版本鏈」。寫不是改值、是疊一個新版本;讀只是沿著鏈往回找「在我這個時間點看得到的那一版」。

⚖️ Tradeoffs

版本可見性規則 — 用 txid 判斷誰看得到哪一版
  • 每個交易拿一個單調遞增的 transaction id (txid);每個 tuple 記錄 created_by(產生它的 txid)與 deleted_by(刪除/更新它的 txid)。
  • 交易啟動時拍一張快照:記下「此刻已 commit 的 txid 集合」與「仍在進行中的 txid」。
  • 一個版本對我可見的條件:產生它的交易在我的快照中已 commit 且不在進行中,且刪除它的交易對我而言尚未 commit(或根本不存在)。
  • 自己交易內的寫入對自己一律可見——讀到自己未提交的修改。
讀不擋寫、寫不擋讀 — MVCC 的核心紅利
  • 純鎖式(如 2PL)下讀者要持有共享鎖、寫者要等讀者放鎖,讀寫互相阻塞,吞吐受限。
  • MVCC 把「過去的版本」留著,讀者永遠有一份可讀的舊版本,因此讀者與寫者可以並行。
  • 寫寫仍會衝突:兩個交易要更新同一列時,後者得等前者 commit/abort(行鎖仍在)——MVCC 不消除寫寫競爭。
垃圾回收 (Vacuum) — 舊版本不會自己消失
  • 當某個舊版本「已經比所有活躍交易的快照都早被刪除」時,它對任何人都不可見,可以安全回收。
  • PostgreSQL 用 VACUUM(與 autovacuum)回收死 tuple;長交易會卡住回收界線,導致版本膨脹 (table bloat)。
  • 實務痛點:一個忘了結束的長交易(如閒置的 analytics 連線)會讓 vacuum 無法推進,磁碟與索引持續膨脹。
與 Snapshot Isolation 的關係 — MVCC 是引擎,SI 是合約
  • Snapshot Isolation 幾乎都用 MVCC 實作:SI 的「整段交易讀同一份一致快照」正是靠版本可見性規則達成。
  • SI 擋掉髒讀、不可重複讀(PostgreSQL 的 SI 連幻讀也擋),但擋不住寫偏斜——因為各交易讀同一份舊快照、寫不同列,彼此看不到。
  • 要在 MVCC 之上達成 Serializable,需再加偵測機制(SSI:偵測危險的讀寫相依,衝突才 abort 重試)。

🔑 Takeaways

✍️ My Notes

2026-07-01

  • Q: MVCC 既然讀不擋寫,為什麼還會有寫寫衝突卡住的情況?
  • A: 因為 MVCC 解決的是「讀」與「寫」之間的隔離——讀者去翻舊版本,不必動到寫者。但兩個交易要更新同一列時,資料庫只能保留一條有效的最新版本鏈,必須序列化這兩個寫入,所以後到的寫者要在那一列的行鎖上等前者 commit 或 abort。MVCC 消除的是讀寫阻塞,不是寫寫競爭;寫寫永遠需要某種仲裁(行鎖或 first-committer-wins)。

📖 Further Reading