可序列化是最強的隔離級別:保證並行交易的結果等同於某種「一個接一個排隊執行」的順序,因此能擋掉所有競爭條件,包含弱級別放行的寫偏斜與幻讀。這頁不重列有哪些異常,而是定位「要真正達成序列化,有哪三條路」。
🧠 Intuition
TIP
弱隔離級別是「擋掉前 N 種異常」的開關,難懂又各家不一致,而且從應用碼很難判斷在某級別下是否安全。可序列化把這件事一次講死:只要結果等同於某個序列執行順序,就沒有任何競爭條件能鑽進來。問題只剩「怎麼達成它而不把效能砍死」。
- 序列化 = 效果等同排隊:不是真的要一個個跑,而是最終結果與某種序列順序一致即可。
- 達成序列化有三條路:實際序列執行、兩階段鎖 (2PL)、可序列化快照隔離 (SSI)。
- 三條路的分野:真的不並行(序列執行)、悲觀擋住衝突(2PL)、樂觀跑完再驗(SSI)。
- 它能擋住弱級別漏掉的:寫偏斜與幻讀在 Snapshot Isolation 下仍會發生,只有序列化保證擋掉。
⚖️ Tradeoffs
實際序列執行 — 直接消除並行,單執行緒跑完每筆交易
- 最直接的做法:在單一執行緒上依序執行所有交易,連鎖的協調開銷都省了,有時反而比支援並行的系統更快。
- 大約 2007 年起才變可行:RAM 夠便宜讓活躍資料集能全載入記憶體,且 OLTP 交易通常短小、只碰少量讀寫。
- VoltDB/H-Store、Redis、Datomic 採此法。
- 硬限制:吞吐量上限就是單一 CPU 核心的處理能力,且要求交易都短小、資料集能進記憶體。
序列執行的配套 — stored procedure 與分割區
- 單執行緒不能讓交易在等網路往返或使用者輸入時佔著執行緒,因此要求應用一次提交整個交易邏輯作為 stored procedure,資料庫一口氣跑完。
- 傳統 stored procedure(PL/SQL、T-SQL)風評不佳;現代實作改用通用語言(VoltDB 用 Java/Groovy、Datomic 用 Clojure、Redis 用 Lua)改善了體驗。
- 分割區 (partition) 讓每個分割區各有一條執行緒,可線性擴展吞吐;但跨分割區交易需額外協調,效能大幅下降(VoltDB 報告跨分割區寫入約 1,000 次/秒)。
兩階段鎖 (2PL) 的定位 — 悲觀鎖式序列化
- 數十年來唯一廣泛使用的可序列化演算法:讀寫都上鎖,讀擋寫、寫也擋讀,與快照隔離「讀不擋寫」正相反。
- 靠 predicate lock / index-range lock 鎖住「查詢條件」而非只鎖既有列,才擋得住幻讀與寫偏斜。
- 代價是死鎖與低吞吐:只要兩交易可能競爭,其一就得等;慢交易持鎖會拖垮一整串。
- 機制細節(成長/收縮期、共享/獨佔鎖、predicate lock)見 two-phase-locking 頁,這裡只把它定位為三條路之一。
可序列化快照隔離 (SSI) — 樂觀偵測,衝突才 abort
- 2008 年由 Cahill 提出,結合快照隔離的效能與序列化的安全:讓交易先自由跑,提交時才檢查衝突,有才中止重試。
- 核心是偵測交易是否基於過時的前提做決策,分兩種:偵測「讀到的 MVCC 快照已過時」(被忽略的寫入後來提交了),與偵測「別人的寫入影響了我先前的讀取」(像 index-range lock 的絆線,但不阻塞、只通知)。
- 相較 2PL:交易不需互相等待,讀寫不阻塞,延遲更可預測;相較序列執行:不受限單核(FoundationDB 把衝突偵測分散到多機)。
- 代價是中止率:長讀寫交易較易撞衝突被中止,因此要求讀寫交易盡量短小(純讀長交易不受影響)。
三法比較與適用場景
- 實際序列執行:最簡單,但受限單核吞吐;適合交易短小、資料集可進記憶體的場景。
- 2PL:數十年標準方案,正確無虞但效能差、延遲不穩定,許多應用刻意避開。
- SSI:最新、樂觀策略,兼顧效能與安全,是目前最有前景的方向。
- 一句話選型:能全記憶體、可分割就序列執行;衝突多且重試貴選 2PL;衝突少、要高並發選 SSI。
🔑 Takeaways
- 可序列化是最強級別:結果等同某種序列順序,因此擋掉包含寫偏斜與幻讀在內的所有競爭條件。
- 實際序列執行用單執行緒 + stored procedure 消除並行,靠 RAM 變便宜與交易短小才可行,上限是單核吞吐。
- 2PL 是悲觀鎖式序列化,用 predicate / index-range lock 擋幻讀,代價是死鎖與低吞吐——細節見專頁。
- SSI 是樂觀法:跑完再驗、偵測過時前提的危險讀寫相依,衝突才 abort 重試,不受單核限制,是現代首選方向。
No notes yet — jot your takeaways or Q&A here.