01

交易是什麼:把一串操作變成一個不可分割的動作

多執行緒同時改同一個物件會發生什麼災難,以及用 synchronized 達成原子操作。

先讀原文段落,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 交易是什麼:把一串操作變成一個不可分割的動作 675 16 TRANSACTIONS AND CONCURRENCY CONTROL 16. 5 Optimistic concurrency control 16. 7 Comparison of methods for concurrency control 16. 8 Summary This chapter discusses the application of transactions and concurrency control to shared objects managed by servers.
白話導讀

多執行緒同時改同一個物件會發生什麼災難,以及用 synchronized 達成原子操作。

為什麼會出錯:同步與原子操作

多執行緒同時改同一個物件會發生什麼災難,以及用 synchronized 達成原子操作。

STEP 1

深度探秘

當兩個人同時改同一個數字

問題的根源:交錯執行

伺服器通常用多執行緒 (threads) 來同時服務很多客戶端,這對效能很好——但也埋下危機。如果兩個客戶端同時對同一個帳戶呼叫 deposit(存款),而這個方法沒有被妥善保護,兩個執行緒的動作就可能任意交錯 (interleaved)

想想看 deposit 內部其實是三步:

  1. 讀出目前餘額
  2. 把餘額加上存款金額
  3. 寫回新餘額

如果兩個執行緒都先做了第 1 步(都讀到舊餘額 $100),再各自加錢寫回,後寫的那個會蓋掉前一個的結果。明明存了兩次,餘額卻只增加一次。

原子操作 (atomic operation)

不會被其他執行緒的並行操作干擾的操作,稱為原子操作

在 Java 裡,可以把方法標記成 synchronized:一個執行緒進入某物件的 synchronized 方法時,這個物件就被「鎖住」,其他執行緒想呼叫它的 synchronized 方法就得等待,直到鎖被釋放。這樣就強迫執行緒一個一個來,物件的內部變數就不會被改亂。

flowchart TD
  A[執行緒一 呼叫 deposit] --> B[取得物件鎖]
  B --> C[讀餘額 加錢 寫回]
  C --> D[釋放鎖]
  E[執行緒二 呼叫 deposit] --> F[想取得鎖 但被佔用]
  F --> G[等待]
  D --> G
  G --> H[取得鎖 接著做]
💡
關鍵

未同步的並行存取會讓操作交錯,產生錯誤;原子操作保證一次只有一個執行緒動到物件。

STEP 2

生活妙喻

共用一本記帳本

一本只有一支筆的記帳本

想像辦公室有一本共用的零用金記帳本,但只有一支筆

  • 沒有規矩時:小明和小華同時想記帳。兩人都先看到本子上寫「餘額 100」,小明心算「加 50 變 150」,小華心算「加 30 變 130」。可是他們各寫各的,最後本子上只剩其中一個數字——另一筆完全消失了。這就是遺失更新的雛形。

  • 有規矩時:規定「拿到筆的人才能寫,寫完才把筆交出去」。小明拿筆時,小華只能在旁邊等;小明寫完「150」放下筆,小華才拿筆,看到的是最新的 150,再加成 180。兩筆都算進去了。

那支筆就是 synchronized 帶來的;「拿到筆才能寫」就是原子操作

還需要互相溝通:wait 與 notify

有時光是排隊還不夠。例如一個共享佇列,客戶端 A 想拿出第一個東西,但佇列是空的——它不該被告知「等等再試」(這樣它得一直來問,很沒效率又不公平),而是應該掛起來等,直到客戶端 B 放了東西進來再叫醒它。

這就像點餐:客人點了一道還沒備料的菜,店家不是叫客人「等下再來點一次」,而是讓客人坐著等,料一到就出菜。Java 的 wait(我先睡,把筆讓出去)和 notify(我放東西進來了,叫醒等的人)就是做這件事。

💡
關鍵

鎖像「只有一支筆」確保不互相蓋掉;wait/notify 像「坐著等出菜」讓執行緒彼此協調而非反覆輪詢。

STEP 3

實用超能力

設計一個不會出錯的共享物件

動手設計:可安全並行的 Account

當你在寫一個會被多個客戶端同時呼叫的伺服器物件時,記住這個原則:

任何會讀寫「可變動的實例變數」的方法,都應該設成原子操作。

以銀行帳戶為例:

public class AccountImpl implements Account {
  private int balance = 0;

  // 加上 synchronized,確保一次只有一個執行緒能改 balance
  public synchronized void deposit(int amount) {
    balance = balance + amount;
  }

  public synchronized void withdraw(int amount) {
    balance = balance - amount;
  }

  public synchronized int getBalance() {
    return balance;
  }
}

用 wait/notify 做一個阻塞佇列

當「消費者要等生產者」時:

public synchronized Object first() {
  while (isEmpty()) {
    wait();          // 佇列空就放掉鎖、掛起自己
  }
  return removeFirst();
}

public synchronized void append(Object o) {
  addLast(o);
  notify();          // 放了東西,叫醒等待者
}

注意兩個重點

  • waitnotify 必須在 synchronized 方法裡用。
  • while 而不是 if 檢查條件——因為被叫醒後,條件可能又被別人改掉了,得再確認一次。

這套機制就是後面章節「鎖」的基礎:當客戶端搶不到鎖時,就讓它 wait,等別人釋放鎖時再 notify 叫醒它。

💡
關鍵

凡是會改可變狀態的方法都要原子化;用 while 包住 wait 來安全地讓執行緒互等。

🔆
生活妙喻:原子操作 ≈ 只有一支筆的共用記帳本

拿到筆的人才能寫、寫完才交筆,確保兩個人的更新不會互相覆蓋,正如 synchronized 確保一次只有一個執行緒動到物件。

🔆
生活妙喻:wait / notify ≈ 餐廳點到還沒備料的菜

店家不叫客人之後再點一次(輪詢),而是讓客人坐著等(wait),料一備好就出菜叫人(notify),避免反覆詢問的浪費與不公平。

本節字彙

原子操作 (atomic operation)
執行過程中不會被其他並行執行緒干擾的操作,效果像是一氣呵成。
🧠 「原子」不可分割——做就做完,不被切斷。
synchronized
Java 關鍵字,讓同一物件上的這些方法一次只能被一個執行緒執行。
🧠 sync = 同步排隊,一個進去其他人在門外等。
wait / notify
讓執行緒在物件上掛起等待(wait),或叫醒等待者(notify)的協調機制。
🧠 wait=我先睡讓位,notify=叫醒你該動了。
兩個執行緒同時對同一帳戶呼叫未同步的 deposit,最可能出現什麼後果?
把方法標記為 synchronized 的主要效果是什麼?
客戶端對一個空佇列呼叫 first,為什麼用 wait 比回覆「稍後再試」更好?

交易與 ACID:全有或全無

交易把一串操作綁成一個單位,並用 ACID 四性質描述它的保證。

STEP 1

深度探秘

把一串操作綁成一個不可分割的步驟

什麼是交易 (transaction)

光是把單一方法原子化還不夠。客戶端常常需要做一連串操作,並且希望這一整串:

  1. 不被其他客戶端的並行操作干擾。
  2. 要嘛全部成功,要嘛在當機時完全沒有效果。

這種「一連串操作被當成一個不可分割單位」的東西,就是交易

經典例子是轉帳:從 A 轉 100 元到 B,其實是 a.withdraw(100) 加上 b.deposit(100) 兩步。如果只做了扣款卻沒入帳(中間當機),錢就憑空消失了。交易保證這兩步「全有或全無」。

原子性的兩個面向

交易作用在可復原物件 (recoverable objects) 上,並保證原子性,這包含:

  • 全有或全無 (all or nothing):成功就全部生效,失敗或中止就完全沒效果。它又分成:
    • 失敗原子性 (failure atomicity):就算伺服器當機,效果仍是原子的。
    • 持久性 (durability):成功後所有效果都存進永久儲存,行程當掉也還在。
  • 隔離性 (isolation):每筆交易不受其他交易干擾,中間的暫態不會被別人看到。
flowchart TD
  A[交易開始] --> B[執行一連串操作]
  B --> C{順利完成?}
  C -->|是| D[commit 全部效果生效並存入永久儲存]
  C -->|否| E[abort 所有效果完全消除]
💡
關鍵

交易把一串操作綁成不可分割的單位,保證全有或全無、且不被其他交易干擾。

STEP 2

生活妙喻

ATM 轉帳與婚禮誓詞

轉帳像「兩人同時點頭」的承諾

想像你在 ATM 把 100 元從活存轉到定存。系統得先從活存扣 100、再往定存加 100。如果機器在扣完款的瞬間斷電——你絕對不希望錢就這樣蒸發。

交易的保證就像婚禮誓詞:「我願意」必須兩個人都說了才算數,只有一方說了不算結婚。交易也是——所有步驟都成功才 commit(正式生效),否則 abort(當作什麼都沒發生)。

ACID:四個字記住交易的承諾

字母 性質 白話解釋
A Atomicity 原子性 全有或全無,不會做一半
C Consistency 一致性 從一個正確狀態變到另一個正確狀態
I Isolation 隔離性 別人看不到你做到一半的暫態
D Durability 持久性 成功後就算當機也救得回來

有趣的是,書中說 C(一致性)通常是程式設計師的責任——例如若有個欄位記錄「所有帳戶餘額總和」,那麼每次 deposit、withdraw 都要記得同步更新它,這樣用 branchTotal 和逐一加總才會得到相同結果。系統本身主要負責 A、I、D。

💡
關鍵

交易像婚禮誓詞——兩方都說「我願意」才算數;ACID 是記住交易四大承諾的口訣。

STEP 3

實用超能力

什麼時候你會需要交易

辨認「需要交易」的情境

只要你的操作符合以下任一特徵,就該考慮包成交易:

  • 跨多筆資料的相依更新:轉帳、訂票(扣庫存 + 建訂單 + 收款)、下單(建單 + 扣點數 + 發通知)。
  • 不能被別人看到半成品:報表查詢時,不該看到轉帳只做了扣款那一半。
  • 失敗要能完整回復:流程中斷時要能乾淨地復原,而不是留下髒資料。

交易長什麼樣

從客戶端角度,一筆交易就是一段「開始—操作—結束」:

Transaction T:
  a.withdraw(100);   // 從 A 扣 100
  b.deposit(100);    // 存進 B
  c.withdraw(200);   // 從 C 扣 200
  b.deposit(200);    // 存進 B

從客戶端看,這整段是單一步驟,把伺服器資料從一個一致狀態轉換到另一個一致狀態。

為什麼不直接「一筆一筆排隊做」就好?

最簡單的隔離方式是讓所有交易完全序列執行(一次一筆)。但這對多人共用的伺服器太慢了——銀行不可能規定全分行只能有一個行員同時作業。所以目標是最大化並行:只要並行的效果跟某種序列執行相同(也就是後面要學的「序列等價」),就允許它們同時跑。

💡
關鍵

凡是跨多筆資料、不能露半成品、失敗要乾淨回復的流程都該用交易;目標是在保證正確下最大化並行。

🔆
生活妙喻:交易的原子性 ≈ 婚禮誓詞要兩人都說我願意

只有所有步驟都成功(雙方都點頭)交易才 commit;任一步失敗就 abort,當作從未發生,正如單方說我願意不算結婚。

🔆
生活妙喻:持久性 (Durability) ≈ 把承諾寫進結婚證書存檔

交易成功後效果寫入永久儲存,就算系統當機重啟也救得回來,像證書歸檔後不會因你失憶而失效。

本節字彙

交易 (transaction)
一連串伺服器操作,被當成一個不可分割、要嘛全成功要嘛全無效的單位。
🧠 把多步操作打包成「一筆」,同生共死。
ACID
交易四性質的口訣:Atomicity、Consistency、Isolation、Durability。
🧠 ACID=交易的『酸性』檢驗,四個都過才合格。
可復原物件 (recoverable object)
伺服器當機後能還原狀態的物件,靠永久儲存保存已提交交易的變更。
🧠 可復原=當機後還救得回來。
轉帳交易做完 a.withdraw(100) 後伺服器當機,b.deposit(100) 還沒執行。交易的「全有或全無」保證會怎麼處理?
ACID 中的哪一個性質,書中特別指出「通常是程式設計師的責任」而非系統自動保證?
「隔離性 (Isolation)」對其他並行交易的保證是什麼?

交易的生命週期與協調者

openTransaction、closeTransaction、abortTransaction 與協調者如何管理交易,以及當機時怎麼處理。

STEP 1

深度探秘

協調者與三個關鍵動作

TID。">協調者 (coordinator)

每一筆交易都由一個協調者建立與管理。協調者提供三個操作給客戶端:

  • openTransaction():開始一筆新交易,回傳一個唯一的交易識別碼 (TID)
  • closeTransaction(trans):表示交易結束,回傳結果是 commit(已提交)或 abort(已中止)。
  • abortTransaction(trans):客戶端主動中止交易,所有效果都要被移除。

客戶端拿到 TID 後,會把它附在交易內的每一個操作上,讓伺服器知道這些操作屬於同一筆交易。當交易作為中介軟體(如 CORBA Object Transaction Service)提供時,TID 可以隱式地自動帶上,不必手動傳。

三種結局

flowchart TD
  A[openTransaction] --> B[一連串 operation]
  B --> C{結局}
  C -->|順利| D[closeTransaction 回傳 commit]
  C -->|客戶端主動| E[abortTransaction 客戶端中止]
  C -->|伺服器決定| F[server aborts 回報錯誤給客戶端]
  • 成功:客戶端 closeTransaction,得到 commit,承諾所有變更已永久記錄。
  • 客戶端中止:客戶端呼叫 abortTransaction
  • 伺服器中止:因衝突或當機等原因,伺服器自己中止它。

後兩種都稱為交易失敗 (failing)

💡
關鍵

協調者用 openTransaction、closeTransaction、abortTransaction 管理交易,並發給每筆交易唯一的 TID。

STEP 2

生活妙喻

餐廳訂位與點餐單號

一張帶號碼的點餐單

把協調者想成餐廳的點餐系統

  • 你入座開始點餐 → openTransaction,系統給你一個桌號/單號 (TID)
  • 接下來你點的每一道菜,都會寫上這個單號(操作帶 TID),廚房才知道哪些菜是同一桌的。
  • 你說「就這些,結帳」→ closeTransaction,廚房確認全部備齊、出餐、收款(commit)。
  • 你中途說「不點了,全部取消」→ abortTransaction(客戶端中止)。
  • 廚房發現某材料用完、無法上整桌的菜 → 由餐廳這邊取消(伺服器中止)。

當機怎麼辦:回到最後一次「結清」的狀態

如果廚房(伺服器)突然停電重開機,它會:

  • 把所有還沒結清(未 commit)的單子作廢。
  • 用紀錄把帳目還原到最近一次成功結清的狀態。

至於客戶端中途落跑(client 當掉),伺服器會給每筆交易一個到期時間 (expiry time),超時還沒結束就自動中止——就像佔著桌位太久沒點餐會被請離。

💡
關鍵

TID 像點餐單號把同一筆交易的操作串起來;當機後伺服器作廢未提交交易、還原到最近成功提交的狀態。

STEP 3

實用超能力

客戶端如何面對交易失敗

客戶端要會處理「交易不見了」

在真實系統中,交易並不保證一定成功,客戶端必須準備好應對:

  1. 伺服器中途當機:客戶端會在某個操作逾時後收到例外 (exception)。如果伺服器當機後被新行程取代,原本的交易就不再有效,客戶端必須在下一個操作收到例外時知道這件事。

  2. 收到例外後要有計畫:客戶端(必要時和真人使用者商量)要決定是重做整筆交易,還是放棄這項任務。

一個健壯的客戶端骨架

int attempts = 0;
while (attempts < MAX_RETRY) {
  trans = openTransaction();
  try {
    a.withdraw(trans, 100);
    b.deposit(trans, 100);
    result = closeTransaction(trans);  // 回傳 commit 或 abort
    if (result == COMMIT) break;       // 成功,結束
  } catch (Exception e) {
    // 伺服器當機或交易失效:記錄、稍後重試
  }
  attempts++;
}

設計重點

  • 不要假設交易一定成功:每個 close 都要檢查回傳的是 commit 還是 abort。
  • 逾時與重試要有上限:避免無限重試。
  • 伺服器端也要保護自己:用到期時間中止那些「客戶端掛了、一直不結束」的交易,避免它們長期佔住資源。
💡
關鍵

交易可能失敗,客戶端要檢查 commit/abort 結果並準備重試或放棄;伺服器用到期時間清掉卡住的交易。

🔆
生活妙喻:協調者與 TID ≈ 餐廳的點餐單號

開始點餐拿到單號(openTransaction 給 TID),每道菜寫上單號(操作帶 TID),廚房才知道哪些屬於同一桌、最後一起出餐結帳。

🔆
生活妙喻:當機後復原 ≈ 停電後回到最近一次結清的帳目

重開機的伺服器作廢所有未結清的單子,用紀錄還原到最後一次成功 commit 的狀態,正如餐廳停電後依結清紀錄對帳。

本節字彙

協調者 (coordinator)
建立並管理交易的元件,提供 open/close/abort 操作並發配 TID。
🧠 協調者=交易的櫃台,負責開單、結帳、取消。
TID (交易識別碼)
協調者發給每筆交易的唯一編號,附在該交易的每個操作上。
🧠 TID=點餐單號,認得出哪些操作是同一筆。
到期時間 (expiry time)
交易的存活上限,超時未完成就被伺服器自動中止,用來清理卡住的交易。
🧠 佔位太久沒動作,到期就請你離開。
客戶端呼叫 openTransaction 後,協調者回傳的 TID 主要用來做什麼?
伺服器當機後被新行程取代,它的恢復程序會把物件還原到什麼狀態?
下列哪一種情況「不」算是交易失敗 (failing)?
02

並行的兩大災難與序列等價

兩個交易交錯執行時,更新可能被覆蓋、查詢可能讀到半完成的狀態。

先讀原文段落,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 並行的兩大災難與序列等價 getBalance(); $200 balance = b. withdraw(balance/10) $80 c. withdraw(balance/10) $280 Figure 16. 6 The inconsistent retrievals problem Transaction V: Transaction W: a.
白話導讀

兩個交易交錯執行時,更新可能被覆蓋、查詢可能讀到半完成的狀態。

遺失更新與不一致擷取

兩個交易交錯執行時,更新可能被覆蓋、查詢可能讀到半完成的狀態。

STEP 1

深度探秘

兩個並行交易交錯時的災難

遺失更新問題 (lost update)

設帳戶 A=100、B=200、C=300。交易 T 把 B 加 10%(從 A 轉),交易 U 也把 B 加 10%(從 C 轉)。正確答案應該是 B 被加兩次 10%,最後 $242。

但若兩交易交錯成這樣:

步驟 T U B 的值
1 balance = b.getBalance() 讀到 200
2 balance = b.getBalance() 也讀到 200
3 b.setBalance(220) 220
4 b.setBalance(220) 220

結果 B 只變成 220(加了 20),不是 242(加了 42)。U 的更新被 T 蓋掉而消失——因為兩者都在對方寫入前讀到了舊值。這就是遺失更新。

不一致擷取問題 (inconsistent retrievals)

A、B 都初始 $200。交易 V 從 A 轉 100 到 B;交易 W 用 branchTotal 加總所有帳戶。若交錯成:

flowchart TD
  A[V 執行 a.withdraw 100  A 變成 100] --> B[W 開始加總 讀 A 得到 100]
  B --> C[W 讀 B 得到 200 此時還沒入帳]
  C --> D[W 算出 A 加 B 等於 300 錯誤]
  D --> E[V 才執行 b.deposit 100  B 變成 300]

W 算出的總和把 A+B 當成 300(正確應是 400),因為它在 V「只轉了一半」(扣了款還沒入帳)時就去加總。這就是不一致擷取。

💡
關鍵

遺失更新是兩交易都讀舊值後互相覆蓋;不一致擷取是查詢交易讀到轉帳只做一半的中間狀態。

STEP 2

生活妙喻

共筆文件與盤點到一半

遺失更新:兩人同時編輯同一行

想像你和同事都打開同一份「未即時同步」的文件,各自看到「庫存:100」。你改成 150 存檔,同事也改成 130 存檔。誰後存,誰的版本就贏,另一個人的修改就這樣不見了。明明兩個人都做了修改,卻只有一筆生效——這就是遺失更新。

不一致擷取:搬東西搬到一半時去盤點

想像倉庫正在把 100 箱貨從 A 區搬到 B 區。搬運工已經從 A 區搬走了(A 區少了 100),但還在路上、還沒放進 B 區。

這時候老闆突然要盤點總庫存:他數 A 區(已少 100)、再數 B 區(還沒多 100),加起來發現少了 100 箱!其實貨根本沒少,只是正卡在「搬運途中」這個中間狀態。

老闆讀到的是一個不該被外人看到的暫態。這正是隔離性要防止的事——別讓人在轉帳做一半時去看總額。

💡
關鍵

遺失更新像兩人各存各的覆蓋彼此;不一致擷取像在貨物搬運途中去盤點,總數一時對不上。

STEP 3

實用超能力

辨認並避開這兩種陷阱

如何辨認你正面臨哪種問題

問題 涉及的操作 症狀
遺失更新 兩個交易都「讀後寫」同一物件 有人的更新憑空消失
不一致擷取 一個查詢交易撞上一個更新交易 查詢讀到改到一半的暫態

共通點:都是因為缺乏並行控制,讓交易看到了彼此的中間狀態。

解法的方向

這兩個問題都能用一個原則治本:讓並行的執行效果,等同於某種「一筆接一筆」的序列執行(也就是下一節的序列等價)。

  • 若遺失更新中 T 完全做完 B 的操作 U 才開始,U 就會讀到 T 寫的新值,不會覆蓋掉。
  • 若不一致擷取中 W 在 V 完成「之前」或「之後」才加總,就不會看到半成品。

寫程式時的具體警訊

當你看到這種「先讀出來、算一算、再寫回去」的模式(read-modify-write)被多個並行者執行時,就要警覺遺失更新。例如:

balance = account.getBalance();   // 讀
balance = balance * 1.1;          // 算
account.setBalance(balance);      // 寫回

這段若沒有交易與並行控制保護,兩個人同時跑就會丟更新。下一節會看到「序列等價」如何當作正確性的判準來根治它。

💡
關鍵

兩種問題都源自缺乏並行控制讓人看到中間狀態;治本之道是讓並行效果等同序列執行(序列等價)。

🔆
生活妙喻:遺失更新 ≈ 兩人各改各存同一份文件

兩人都看到同一個舊值各自修改存檔,後存的覆蓋先存的,一個人的修改憑空消失,正如兩交易都讀舊值後互相覆蓋。

🔆
生活妙喻:不一致擷取 ≈ 貨物搬運途中去盤點

貨已離開 A 區但還沒到 B 區,此時盤點總數會短少;正如查詢交易在轉帳只完成扣款時加總,得到不一致的結果。

本節字彙

遺失更新 (lost update)
兩交易都讀到同一舊值再寫回,後寫者覆蓋前者,造成一筆更新消失。
🧠 都讀舊值各自寫,後者蓋前者=更新不見了。
不一致擷取 (inconsistent retrievals)
查詢交易在另一更新交易只做一半時讀取,得到對不起來的結果。
🧠 在『做一半』時去查,數字當然兜不攏。
讀後寫 (read-modify-write)
先讀出值、計算、再寫回的常見模式,並行時最容易發生遺失更新。
🧠 讀→改→寫,三步若被插隊就出事。
兩個交易都先讀帳戶 B 的餘額 200,各自加 10% 後寫回 220,最後 B 是 220 而非 242。這是什麼問題?
交易 V 從 A 轉 100 到 B(已扣 A、還沒入 B)時,交易 W 去加總所有餘額,得到偏少的結果。這是什麼問題?
遺失更新最常發生在哪種操作模式被多個並行交易執行時?

序列等價與操作衝突

什麼樣的交錯等同於一個接一個執行,以及如何用 read/write 衝突規則來判定。

STEP 1

深度探秘

什麼樣的交錯算「正確」

序列等價 (serial equivalence)

如果每筆交易單獨執行時都是正確的,那麼把它們一筆接一筆(某種順序)執行,合起來的效果也會正確。

一種交易操作的交錯,若其合併效果與「把這些交易一筆接一筆依某順序執行」相同,就稱為序列等價的交錯 (serially equivalent interleaving)

以序列等價當作「並行執行正確」的判準,就能同時避免遺失更新與不一致擷取——因為序列執行裡,後者一定讀到前者寫好的值,查詢也不會撞上半成品。

操作衝突 (operation conflicts)

要判斷一個交錯是否序列等價,得先看哪些操作會衝突。把操作簡化成 read 與 write:

操作 A 操作 B 衝突? 原因
read read 兩個讀的效果與順序無關
read write 讀和寫的效果取決於誰先
write write 兩個寫的效果取決於誰先

兩操作衝突,意思是它們的合併效果取決於執行順序

序列等價的精確條件

兩筆交易要序列等價,充分必要條件是:它們在所有共同存取的物件上,所有成對的衝突操作都以相同的順序執行。

flowchart TD
  A[找出兩交易共同存取的物件] --> B[列出所有成對的衝突操作]
  B --> C{每對衝突在各物件上順序一致?}
  C -->|是| D[序列等價 正確]
  C -->|否| E[非序列等價 可能出錯]
💡
關鍵

序列等價=交錯效果等同某種序列執行;判定法是讓所有衝突操作在所有共享物件上以相同順序執行。

STEP 2

生活妙喻

兩個廚師、兩個鍋子

read-read 不衝突:兩人同時看食譜

兩個廚師同時「讀」同一張食譜,誰先看誰後看都沒差,食譜不會因此改變——這就是 read-read 不衝突

而只要其中一人要在食譜上「改」字(write),順序就重要了:你是看了原版才改,還是看了改過的版本,結果完全不同。這就是 read-write 與 write-write 會衝突

序列等價:兩個鍋子要「一致地讓位」

假設廚師 T 和 U 都要用 B 鍋和某個別的鍋。序列等價要求:

如果在 B 鍋上是「T 先用、U 後用」,那麼在所有兩人共用的鍋子上,都必須是「T 先、U 後」。

不能在 B 鍋讓 T 先,卻在另一個鍋讓 U 先——那樣順序自相矛盾,等於沒有一個一致的「誰先誰後」,就不是序列等價

書中的反例:T 對物件 i 全部先做、U 對物件 j 全部先做。雖然各自看起來有序,但「i 上 T 先、j 上 U 先」順序相反,所以不是序列等價。正確的序列等價要嘛「兩個物件都 T 先」,要嘛「兩個物件都 U 先」。

💡
關鍵

兩人同時看食譜不衝突(read-read);但誰先誰後在所有共用鍋子上必須一致,否則不算序列等價。

STEP 3

實用超能力

用衝突規則檢查一個交錯

實戰:判斷一個交錯是否序列等價

看這兩筆交易(i、j 是兩個物件):

T: x = read(i); write(i,10); write(j,20);
U: y = read(j); write(j,30); z = read(i);

某個交錯實際執行順序為:

x = read(i)      (T)
write(i,10)      (T)
y = read(j)      (U)
write(j,30)      (U)
write(j,20)      (T)
z = read(i)      (U)

檢查步驟

  1. 在物件 i 上:T 的 read(i)write(i,10) 都在 U 的 read(i) 之前 → 在 i 上是「T 先於 U」。
  2. 在物件 j 上:U 的 read(j)write(j,30) 都在 T 的 write(j,20) 之前 → 在 j 上是「U 先於 T」。
  3. 兩個物件上的順序相反!所以這個交錯不是序列等價

帶走的判定流程

序列等價的兩個合法可能:(1) 兩物件上都 T 先於 U;或 (2) 兩物件上都 U 先於 T。只要出現「一個物件 T 先、另一個 U 先」就破功。

這正是後面三大並行控制協定的共同目標——它們都在想辦法把交易對物件的存取序列化,讓所有衝突操作順序一致:鎖用「到達順序」、時戳用「開始時間」、樂觀法用「提交前驗證」。

💡
關鍵

判定法:找出每個共享物件上兩交易的衝突順序,若所有物件上順序一致才序列等價,否則就破功。

🔆
生活妙喻:read-read 不衝突 ≈ 兩個廚師同時看同一張食譜

兩人只是讀、誰先誰後都不改變食譜內容,所以不衝突;一旦有人要在食譜上改字,順序就有差,就變成衝突。

🔆
生活妙喻:序列等價的順序一致 ≈ 在所有共用鍋子上誰先誰後要一致

若 B 鍋是 T 先用,其他共用鍋也得 T 先用;若某鍋反過來讓 U 先,順序自相矛盾就不是序列等價。

本節字彙

序列等價 (serial equivalence)
一種交錯,其合併效果與某種一筆接一筆的序列執行相同,是並行正確的判準。
🧠 交錯跑出來的結果,跟排隊一個個跑一樣=序列等價。
操作衝突 (operation conflict)
兩操作的合併效果取決於執行順序;read-write 與 write-write 衝突,read-read 不衝突。
🧠 只要有寫,順序就重要;兩個讀則無所謂。
衝突順序一致
兩交易在所有共享物件上的成對衝突操作都以相同先後次序執行,是序列等價的充要條件。
🧠 誰先誰後,全場物件都得說同一個故事。
下列哪一對操作「不」衝突?
兩筆交易要序列等價的充分必要條件是什麼?
某交錯在物件 i 上是「T 先於 U」,但在物件 j 上是「U 先於 T」。這個交錯是否序列等價?
03

可從中止復原,與巢狀交易

中止會讓別人看到不存在的值,如何用嚴格執行與暫存版本避免後患。

先讀原文段落,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 可從中止復原,與巢狀交易 t’s request is never rejected. As pairs of read operations from different transactions do not conflict, an attempt to set a read lock on an object with a read lock is always successful. All the transactions reading the same object share its read lock – for this reason, read locks are sometimes called shared locks. The operation conflict rules tell us that: 1.
白話導讀

中止會讓別人看到不存在的值,如何用嚴格執行與暫存版本避免後患。

髒讀、連鎖中止與嚴格執行

中止會讓別人看到不存在的值,如何用嚴格執行與暫存版本避免後患。

STEP 1

深度探秘

中止帶來的三個新麻煩

為什麼序列等價還不夠

上一章保證了交易「序列等價」就不會遺失更新或不一致擷取。但別忘了交易可能中止 (abort)——這又帶來新問題。即使在序列等價的執行下,這些問題仍會發生。

髒讀 (dirty read)

隔離性要求交易不該看到別人未提交的狀態。考慮:T 把 A 餘額加 10,U 接著讀到這個新值、再加 20 並提交。然後 T 中止了——A 被還原成原值。但 U 已經根據一個「從未真正存在的值」提交了,且無法回頭。我們說 U 做了髒讀

連鎖中止 (cascading aborts)

為了可復原,U 應該延遲提交,直到它觀察過的交易(T)都提交為止;若 T 中止,U 也得中止。但若還有其他交易讀過 U 寫的值,它們也得跟著中止……一路連鎖下去,這叫連鎖中止

避免連鎖中止的辦法:交易只能讀已提交交易寫的值

過早寫入 (premature writes)

這牽涉兩個交易對同物件的 write。許多系統用「前像 (before image)」來實作中止(中止時還原成寫之前的值)。但若 T、U 接連寫同一物件,前像就可能對應錯,導致中止後得到錯誤的值。

flowchart TD
  A[T 寫 A 但尚未提交] --> B[U 讀或寫 A]
  B --> C{T 之後中止?}
  C -->|是| D[U 基於不存在的值 出錯]
  C -->|否| E[沒事]
💡
關鍵

交易會中止,因而衍生髒讀、連鎖中止與過早寫入;根源都是看到或覆蓋了未提交的值。

STEP 2

生活妙喻

鉛筆草稿與骨牌

髒讀:照抄別人還沒確定的草稿

同事在白板上用鉛筆寫了「預算 = 110」(還沒確定),你看到後就拿去做了你的報表並交了出去。結果同事說「啊那只是草稿」把它擦掉改回 100。你的報表已經送出去、收不回了——你引用了一個從未正式存在的數字。這就是髒讀。

連鎖中止:推倒的骨牌

如果你的報表又被別人引用、那個人的東西又被下一個人引用……一旦最初的草稿被擦掉,所有引用過它的人都得作廢重做。一張牌倒下,整排骨牌跟著倒——這就是連鎖中止。

防骨牌的規矩:只准引用「已經正式拍板(提交)」的數字,不准抄草稿。這樣最初那張牌就算倒了,也沒人靠在它身上。

嚴格執行:等對方拍板再動

要同時防髒讀和過早寫入,最乾脆的規矩是:

對某物件的讀或寫,都延遲到所有先前寫過它的交易都已提交或中止為止。

這就像「你想用這份文件?等正在改它的人按下『完成』或『取消』,你再動」。這種執行方式叫嚴格執行 (strict execution),它確保了隔離性。

💡
關鍵

髒讀像照抄別人的鉛筆草稿;連鎖中止像推倒骨牌;嚴格執行=等先前的寫者拍板後你才動,根治兩者。

STEP 3

實用超能力

嚴格執行與暫存版本

兩個讓中止安全的關鍵機制

1. 嚴格執行 (strict execution)

規則:讀和寫都延遲,直到所有先前寫過同物件的交易都已 commit 或 abort

  • 防髒讀:你不會讀到還沒提交的值(要嘛已提交、要嘛已撤銷)。
  • 防過早寫入:你不會覆蓋一個還沒定案、之後可能被還原的值。

這正是後面「嚴格兩階段鎖」的理論基礎——把鎖一直握到交易結束。

2. 暫存版本 (tentative versions)

要讓中止能乾淨地撤銷,所有更新都先做在暫存版本上(放在揮發性記憶體中,每筆交易有自己私有的一份):

  • 交易的所有寫入都存到自己的私有暫存版本。
  • 讀取優先從自己的暫存版本取,沒有才去讀真正物件。
  • 提交時:才把暫存版本一次套用到真正物件(並寫入永久儲存),這一步要排除其他交易存取。
  • 中止時:直接丟掉暫存版本,真正物件毫髮無傷。
// 概念示意
交易 T 的私有暫存區: { A: 110 }   // 還沒套用
真正物件:            { A: 100 }   // 維持原值
// 若 T commit → 真正物件變 110
// 若 T abort  → 丟掉暫存區,真正物件仍是 100

帶走的設計準則

想讓「可以反悔」的交易不傷到別人:先寫在暫存版本(反悔零成本),並用嚴格執行讓大家都等先前的寫者定案後才動。

💡
關鍵

嚴格執行讓讀寫都等先前寫者定案,暫存版本讓更新先寫私有副本、提交才套用、中止可零成本丟棄。

🔆
生活妙喻:髒讀 ≈ 照抄白板上的鉛筆草稿

你引用了同事還沒拍板的草稿值並交出報表,對方後來擦掉改回原值,你卻收不回,正如讀到後來中止交易寫的未提交值。

🔆
生活妙喻:暫存版本 ≈ 先在自己的草稿紙上算

更新先寫在私有草稿(暫存版本),確定無誤才謄上正本(提交套用到真正物件),不滿意就揉掉草稿(中止丟棄),正本完全不受影響。

本節字彙

髒讀 (dirty read)
一個交易讀到另一交易尚未提交、後來又中止的值,因而依據不存在的值做事。
🧠 讀到沒拍板的草稿=讀到髒資料。
連鎖中止 (cascading aborts)
一交易中止導致讀過它的交易也得中止,一路連鎖;靠只讀已提交值來避免。
🧠 一張骨牌倒,整排跟著倒。
嚴格執行 (strict execution)
讀寫都延遲到先前寫過該物件的交易已提交或中止,藉此防髒讀與過早寫入。
🧠 嚴格=先前寫者沒拍板,你就別動。
交易 U 讀了 T 尚未提交時寫入的值並據此提交,之後 T 卻中止。U 發生了什麼?
要避免連鎖中止,交易應該遵守什麼規則?
嚴格執行 (strict execution) 的規則是什麼?

巢狀交易:把交易當積木組合

交易內可再開子交易,子交易能獨立失敗,提供更多並行與韌性。

STEP 1

深度探秘

交易裡再開交易

什麼是巢狀交易 (nested transaction)

巢狀交易擴充了原本的交易模型,允許一筆交易由其他交易組成。這樣交易就像可以組裝的模組。

  • 最外層的交易叫頂層交易 (top-level transaction)
  • 其他都叫子交易 (subtransaction)

例如 T 開出子交易 T1、T2;T1 又開出 T11、T12;T2 開出 T21,T21 再開 T211,形成一棵樹。

子交易對它的父交易而言是原子的(就失敗與並行存取而言)。同層的子交易(如 T1、T2)可並行執行,但對共同物件的存取會被序列化。

為什麼要巢狀(相對於「平坦交易 flat transaction」)

平坦交易所有工作都在同一層,無法只提交或中止其中一部分。巢狀交易有兩大優點:

  1. 更多並行:同層子交易可並行,跑在不同伺服器上時甚至能平行運算。例如 branchTotal 對每個帳戶呼叫 getBalance,每個都當作子交易就能並行(彼此不衝突)。
  2. 可獨立提交或中止:更健壯。例如「寄信給一串收件人」可拆成多個子交易,某些失敗時父交易仍可記錄並提交成功的那些,之後再重試失敗的。
flowchart TD
  T[T 頂層交易] --> T1[T1 子交易]
  T --> T2[T2 子交易]
  T1 --> T11[T11]
  T1 --> T12[T12]
  T2 --> T21[T21]
  T21 --> T211[T211]
💡
關鍵

巢狀交易讓交易由子交易組成,同層可並行、可獨立提交或中止,帶來更多並行與韌性。

STEP 2

生活妙喻

公司專案的層層分工

一個大專案,分包給好幾個小組

把頂層交易想成一個大專案,子交易是分派出去的子任務

  • 大專案(頂層 T)拆成「設計」「開發」兩個子任務(T1、T2)。
  • 「開發」又拆成「前端」「後端」(T21、T211)。
  • 不同子任務可以同時進行(同層並行);如果在不同部門做,等於平行作業。

「暫時拍板」與「最終定案」

關鍵規則是子交易的提交只是暫時提交 (provisional commit)

  • 子任務做完,自己決定「暫時拍板」或「放棄」——但放棄是最終的,拍板只是暫時的。
  • 子任務的成果在整個大專案(頂層)正式拍板前都不算數
  • 父任務取消,底下所有子任務(即使已暫時拍板)也一律作廢。
  • 某子任務放棄時,父任務可以自己決定要不要跟著取消——例如寄信例子裡,少數收件人失敗,父交易仍可選擇提交成功的部分。

就像專案經理說:「就算前端組做完了,要等我(整個專案)簽字,你們的成果才正式生效;萬一整個專案喊停,已經做的也都不算。」

💡
關鍵

頂層像大專案、子交易像分包子任務;子交易只能『暫時拍板』,頂層正式定案前都不算數,放棄則是最終的。

STEP 3

實用超能力

巢狀交易的提交規則

提交規則(有點微妙,但很關鍵)

  1. 交易必須在它的子交易都完成後才能提交或中止。
  2. 子交易完成時,獨立決定暫時提交或中止;中止的決定是最終的
  3. 父交易中止時,它所有子交易都中止——即使那些子交易已暫時提交。例如 T2 中止,T21、T211 也得中止。
  4. 子交易中止時,父交易可自行決定要不要中止。例如 T 仍選擇提交,即使 T2 已中止。
  5. 頂層交易提交時,所有暫時提交且祖先都沒中止的子交易才能真正提交。例如 T 提交讓 T1、T11、T12 提交,但 T21、T211 不行(因為父 T2 中止了)。

重點:子交易的效果在頂層交易提交前都不是永久的

實例:轉帳拆成子交易

Transfer 從 B 轉 100 到 A:
  subtx1: a.deposit(100)
  subtx2: b.withdraw(100)   // 若 B 透支則中止
  • 若 withdraw 子交易因透支而中止,但 deposit 已暫時提交——
  • 頂層 Transfer 多半會決定中止:父交易一中止,deposit 子交易也被撤銷,所有效果歸零。

為什麼在分散式系統特別有用

子交易可在不同伺服器並行執行——這是巢狀交易在分散式環境的最大價值(第 17 章會深入)。當一筆任務天然能拆成「對不同資源、彼此獨立」的子工作時,巢狀結構讓你又快又健壯。

💡
關鍵

父中止則全部子交易中止;子中止則父可自行抉擇;頂層提交後祖先未中止的暫時提交才真正生效。

🔆
生活妙喻:暫時提交 (provisional commit) ≈ 子任務做完但要等專案簽字

子交易暫時拍板的成果,要等整個頂層交易正式定案才生效;專案喊停,已做完的子任務也一律作廢。

🔆
生活妙喻:同層子交易並行 ≈ 不同部門同時做各自的子任務

同層子交易彼此獨立時可並行甚至跨伺服器平行,像 branchTotal 對各帳戶並行讀取,互不衝突。

本節字彙

巢狀交易 (nested transaction)
由其他子交易組成的交易,最外層為頂層交易,可組裝成樹狀結構。
🧠 交易裡還能再開交易,像俄羅斯娃娃。
暫時提交 (provisional commit)
子交易完成時的暫定拍板,要等頂層交易提交且祖先未中止才真正生效。
🧠 暫時=還沒蓋正式章,等頂層簽字。
平坦交易 (flat transaction)
傳統單層交易,所有工作在同一層,無法只提交或中止其中一部分。
🧠 平坦=一層到底,要嘛全成要嘛全敗。
在巢狀交易中,子交易完成時做的提交決定有什麼特性?
若父交易 T2 中止,而它的子交易 T21、T211 已經暫時提交,會怎樣?
當某個子交易中止時,它的父交易可以怎麼做?
04

鎖:最常用的並行控制武器

鎖如何序列化存取、嚴格兩階段鎖的規則,以及多讀單寫的共享鎖機制。

先讀原文段落,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 鎖:最常用的並行控制武器 read operations in the two-version locking scheme are delayed only while the transactions are being committed, rather than during the entire execution of transactions – in most cases, the commit protocol takes only a small fraction of the time required to perform an entire transaction. On the other hand, read operations of one transaction can cause delays in committing other transactions. Hierarchic locks • In some applications, the granularity suitable for one operation is not appropriate for another operation. In our banking example, the majority of the operations require locking at the granularity of an account.
白話導讀

鎖如何序列化存取、嚴格兩階段鎖的規則,以及多讀單寫的共享鎖機制。

兩階段鎖與讀寫鎖

鎖如何序列化存取、嚴格兩階段鎖的規則,以及多讀單寫的共享鎖機制。

STEP 1

深度探秘

用鎖把存取序列化

鎖如何達成序列等價

最簡單的序列化機制是互斥鎖 (exclusive lock):伺服器在某物件即將被交易使用前鎖住它;若該物件已被別的交易鎖住,後來者就等待直到解鎖。鎖把對該物件的存取序列化了。

兩階段鎖 (two-phase locking)

要保證序列等價,所有衝突操作必須以相同順序執行。為此規定:

交易一旦釋放任何一個鎖,就不能再取得新鎖。

於是每筆交易分成:

  • 成長期 (growing phase):只取得新鎖。
  • 收縮期 (shrinking phase):只釋放鎖。

嚴格兩階段鎖 (strict two-phase locking)

因為交易可能中止(會有髒讀、過早寫入),需要嚴格執行。做法是:所有鎖都握到交易提交或中止為止。提交時為了可復原,鎖還要握到所有更新都寫入永久儲存。

讀寫鎖:多讀單寫

單一互斥鎖太嚴——read-read 其實不衝突。所以改用兩種鎖(多讀單寫 many readers/single writer):

已設的鎖 請求 read 請求 write
OK OK
read(共享) OK wait
write wait wait

讀鎖可被多個交易共享(又稱共享鎖),寫鎖則互斥。

flowchart TD
  A[交易要存取物件] --> B{物件已被鎖?}
  B -->|沒鎖| C[上鎖 操作進行]
  B -->|有衝突鎖| D[等待 直到解鎖]
  B -->|有非衝突的讀鎖| E[共享該鎖 操作進行]
  C --> F[交易結束 commit或abort 才解鎖]
  E --> F
💡
關鍵

鎖把存取序列化;兩階段鎖先成長後收縮,嚴格兩階段鎖把鎖握到結束,讀寫鎖達成多讀單寫。

STEP 2

生活妙喻

會議室與圖書館借書

互斥鎖:一次一組人的會議室

互斥鎖像會議室——一組人進去就反鎖,其他人只能在門外等到他們出來。這保證裡面的討論不被打斷(存取序列化)。

兩階段鎖:先借齊、再一起還

「成長期只借、收縮期只還,且一旦開始還就不能再借」這條規矩,可以想成:

你出門辦事,該借的工具一開始全借齊;一旦開始歸還任何一樣,就代表你進入收尾,不准再回頭借新東西。

為什麼?如果你還了 A 工具、別人拿走改了它,你又回頭借 B——你看到的世界就可能前後不一致,破壞序列等價。

讀寫鎖:圖書館的閱覽 vs 修訂

讀寫鎖像圖書館的一本書:

  • 很多人可以同時『閱讀』(共享讀鎖)——大家看的是同一份內容,不衝突。
  • 要『修訂』這本書時必須獨佔(寫鎖)——修訂期間別人不能讀也不能改,否則會讀到改到一半的內容。

所以是「多讀單寫」:讀的人可以一起來,寫的人必須一個人關起門來改。

💡
關鍵

互斥鎖像會議室一次一組;兩階段鎖像先借齊工具、開始還就別再借;讀寫鎖像圖書館多人共讀、修訂須獨佔。

STEP 3

實用超能力

鎖升級、粒度與實作

鎖升級 (lock promotion)

防遺失更新的技巧:交易讀物件時先設讀鎖,要寫同物件時再升級成寫鎖

鎖只能升級(變更獨佔)不能降級(變更寬鬆)。降級會讓別人在交易還沒結束時插入不符序列等價的操作。

注意:若讀鎖正被多個交易共享,持有者無法直接升級成寫鎖(會跟別人的讀鎖衝突),只能請求寫鎖並等其他讀鎖釋放。

粒度 (granularity) 很重要

鎖的粒度該盡量小——只鎖每個操作真正用到的部分。

反例:若銀行把鎖加在「整個分行的所有帳戶」上,那同一時間只有一個行員能作業,完全不可接受。

所以 deposit/withdraw 只鎖一個帳戶餘額,branchTotal 才需要鎖全部。

鎖的實作骨架

鎖由 LockManager 管理(用雜湊表存所有 Lock)。每個 Lock 記錄:被鎖物件、目前持有者的 TID(共享鎖可多個)、鎖的型別。

acquire(trans, lockType):
  while (另一交易以衝突模式持有此鎖) wait();
  if (沒人持有) 加入持有者, 設型別
  else if (別人持有且可共享) 把自己加入持有者
  else if (自己已持有但需更獨佔) promote() 升級

release(trans):
  從持有者移除 trans
  設型別為 none
  notifyAll()   // 叫醒所有等待者,因共享讀鎖可能多人能繼續

whilewait、釋放時 notifyAll:因為被叫醒的人不一定都能繼續,得各自再檢查條件。

💡
關鍵

鎖可升不可降;粒度要盡量小以保並行;鎖由 LockManager 管理,用 while+wait、釋放時 notifyAll。

🔆
生活妙喻:讀寫鎖(多讀單寫) ≈ 圖書館一本書多人共讀、修訂須獨佔

很多人能同時閱讀同一份內容(共享讀鎖不衝突),但要修訂時必須一人關門獨佔(寫鎖),以免別人讀到改到一半的內容。

🔆
生活妙喻:兩階段鎖 ≈ 出門前借齊工具,開始還就別再借

成長期只借(取得鎖)、收縮期只還(釋放鎖),一旦開始歸還就不再借新工具,避免看到被別人改過的不一致世界。

本節字彙

兩階段鎖 (two-phase locking)
交易先只取得鎖(成長期)後只釋放鎖(收縮期),釋放後不再取得新鎖。
🧠 先只借後只還,分成成長與收縮兩階段。
嚴格兩階段鎖 (strict two-phase locking)
把所有鎖握到交易提交或中止為止,以達成嚴格執行、防髒讀與過早寫入。
🧠 嚴格=鎖一握就握到底,不到結束不放。
鎖升級 (lock promotion)
把已持有的鎖轉成更獨佔的鎖(如讀鎖升寫鎖);只能升不能降。
🧠 鎖只能往更緊升級,不能往更鬆降級。
兩階段鎖 (two-phase locking) 的核心規則是什麼?
在讀寫鎖(多讀單寫)方案中,下列哪種情況可以同時進行?
嚴格兩階段鎖 (strict two-phase locking) 與一般兩階段鎖的差別是什麼?

死結:互相等待的僵局

鎖如何導致死結、用等待圖偵測循環,以及預防、偵測與逾時三種化解方式。

STEP 1

深度探秘

鎖帶來的副作用

死結 (deadlock) 是什麼

用鎖可能造成死結。經典場景:

  • 交易 T 先鎖住 A,再想鎖 B。
  • 交易 U 先鎖住 B,再想鎖 A。
  • 結果:T 等 U 放 B,U 等 T 放 A——兩者互相等待,誰都動不了

死結是一群交易中,每個成員都在等另一個成員釋放鎖的狀態。

等待圖 (wait-for graph)

用一張圖來表示等待關係:節點是交易,邊代表「等待」——從 T 到 U 的邊表示「T 在等 U 釋放鎖」。

上面的例子裡,T→U 且 U→T,形成一個循環 (cycle)

等待圖中有循環,就代表死結。

flowchart LR
  T[交易 T] -->|等待| U[交易 U]
  U -->|等待| T

更複雜時(如 T 等 U 與 V、V 等 W、W 等 T 等等),一個交易可能同時牽涉在多個循環裡。要打破死結,只要中止循環中的一個交易、釋放它的鎖,循環就斷了。

為什麼互動式程式特別容易死結

互動程式裡的交易可能持續很久(使用者慢慢操作),導致很多物件被鎖住又遲遲不放,更容易卡住其他客戶端。

💡
關鍵

死結是一群交易互相等待對方放鎖;用等待圖表示,圖中有循環即代表死結,中止循環中一交易即可打破。

STEP 2

生活妙喻

兩台車的窄巷對峙

窄巷裡頭對頭的兩台車

想像一條只能單向通過的窄巷,兩台車從兩端開進來,在中間頭對頭卡住:

  • A 車要往前,得等 B 車先退。
  • B 車要往前,也得等 A 車先退。
  • 兩個都不退,永遠卡在那裡

這就是死結。等待圖就是把「A 等 B、B 等 A」畫成一個圈。要解決,只能請其中一台車倒退離開(中止一個交易)——這樣另一台就能通過了。

為什麼會形成圈

死結的本質是「環狀的相互依賴」:每個人手上握著別人想要的東西,又在等別人手上的東西。只要這個「想要」的箭頭連成一圈,就解不開。

久候不一定是死結

注意:等很久 ≠ 一定死結。也許對方只是還在忙、待會就會放手。真正的死結是「形成了封閉的循環,誰都不可能先放」。這個區別很重要——後面會看到用「逾時」來猜死結時,常會誤殺其實沒死結、只是慢的交易。

💡
關鍵

死結像窄巷裡兩車頭對頭互不相讓,等待圖連成一圈就無解;但等很久不一定是死結,可能只是對方還在忙。

STEP 3

實用超能力

預防、偵測、逾時三種解法

化解死結的三條路

1. 預防 (prevention)——治本但代價高

  • 一開始就鎖住所有要用的物件(單一原子步驟):不會死結,但嚴重限制並行,且常無法預知會用到哪些物件(互動式瀏覽根本說不準)。
  • 依固定順序請求鎖:可避免循環,但會造成過早上鎖、降低並行。
  • 升級鎖 (upgrade lock):CORBA 引入的第三種鎖,持有者可讀該資料,但與其他交易的升級鎖衝突,避免「兩個交易都先拿讀鎖再搶著升寫鎖」造成的死結。

2. 偵測 (detection)——找循環再中止

鎖管理員維護等待圖,setLock 時加邊、unLock 時刪邊,定期(或每次加邊時)檢查是否有循環。發現死結就選一個交易中止打破循環。選誰中止不簡單,可參考交易的年紀和它牽涉幾個循環

3. 逾時 (timeout)——常用但有缺點

每個鎖有一段「不可侵犯期」,過後變成脆弱 (vulnerable)。若此時有別的交易在等這個物件,鎖就被打破、等待者繼續,被打破鎖的交易通常被中止。

flowchart TD
  A[偵測到死結] --> B[在循環中選一個交易]
  B --> C[中止它 釋放其鎖]
  C --> D[移除對應節點與邊 循環被打破]

逾時法的痛點

  • 最糟的是:交易其實沒有死結,只是鎖剛好變脆弱又有人在等,就被冤枉中止。
  • 系統過載時逾時暴增,長交易容易被懲罰。
  • 逾時長度也很難拿捏。相較之下,偵測法是「真的有死結才中止,且能挑選犧牲者」。
💡
關鍵

化解死結三法:預防(治本但限制並行)、偵測(找循環再中止選定交易)、逾時(常用但會誤殺沒死結的交易)。

🔆
生活妙喻:死結與等待圖 ≈ 窄巷裡兩台車頭對頭

A 等 B 退、B 等 A 退,箭頭連成一圈誰都不能先動,正如等待圖中的循環;只能請一台車倒退(中止一交易)來解圍。

🔆
生活妙喻:逾時法的誤殺 ≈ 排隊太久就被趕走,其實前面只是慢

逾時把『等很久』當成死結而中止交易,但對方可能只是還在忙、根本沒死結,於是無辜的交易被冤枉中止。

本節字彙

死結 (deadlock)
一群交易每個都在等另一個釋放鎖,形成誰都無法前進的僵局。
🧠 你等我、我等你,全卡死。
等待圖 (wait-for graph)
以交易為節點、等待關係為有向邊的圖;圖中有循環即代表死結。
🧠 畫出誰等誰,連成一圈就是死結。
逾時 (timeout)
給鎖一段不可侵犯期,過後若有人在等就打破鎖並中止持有者,是常用但會誤殺的解法。
🧠 等太久就破鎖,但可能冤枉沒死結的人。
交易 T 鎖住 A 後想鎖 B,交易 U 鎖住 B 後想鎖 A,兩者互相等待。這是什麼狀態?
在等待圖 (wait-for graph) 中,什麼情況代表發生了死結?
用死結「偵測」法發現死結後,標準做法是什麼?

提升並行:雙版本鎖與階層鎖

用更細緻的鎖策略增加並行:延後設互斥鎖的雙版本鎖,與混合粒度的階層鎖。

STEP 1

深度探秘

讓鎖更聰明,擠出更多並行

即使粒度已最小,仍有空間

就算讀寫鎖、粒度也夠細了,還是有提升並行的餘地。書中介紹兩種進階策略:雙版本鎖(延後設互斥鎖到提交)與階層鎖(混合粒度)。

雙版本鎖 (two-version locking)

這是一種樂觀色彩的方案:允許一個交易寫入暫存版本,同時其他交易仍從已提交版本讀取。讀取只在「另一交易正在提交同一物件」時才需要等。

它用三種鎖——讀鎖、寫鎖、提交鎖 (commit lock)

已設的鎖 設 read 設 write 設 commit
OK OK OK
read OK OK wait
write OK wait
commit wait wait
  • 讀鎖:除非物件有 commit 鎖,否則都能設。
  • 寫鎖:除非物件有 write 或 commit 鎖,否則都能設。
  • 交易要提交時,協調者把它的所有寫鎖轉成 commit 鎖;若物件還有別人的讀鎖,得等那些讀交易完成。

取捨:讀操作只在「提交期間」被擋(而非整個交易期間),通常很短;但讀操作反而可能拖延別人的提交。等待提交時還可能死結,故有時需中止。

💡
關鍵

雙版本鎖延後到提交才設提交鎖,讓讀者邊讀已提交版本、寫者邊寫暫存版本,讀取只在提交期間被擋。

STEP 2

生活妙喻

草稿與正本、行事曆的週與時段

雙版本鎖:改稿時別人還能看正本

想像一份公告:撰稿人正在改草稿版,但其他人此刻仍可閱讀牆上那份已定案的正本,完全不受影響。只有在撰稿人要把草稿「正式貼上牆取代正本」(提交)的那短短一刻,閱讀者才需要稍等一下。

相比傳統寫鎖「我一開始改你就不能看」,雙版本鎖把「不能看」的時間壓縮到只剩提交那一瞬間,所以讀取被擋得更少。

階層鎖:行事曆的「週/日/時段」

想像一本行事曆,結構是:週 → 每天一頁 → 每天再切成時段

  • 想「看整週」→ 在最頂層(週)設讀鎖,自動防止任何人改底下的時段。
  • 想「填一個時段的約會」→ 只在那個時段設寫鎖。

這樣大範圍操作鎖一個大節點就好(省下大量小鎖),小範圍操作鎖細節點以保並行。同理,銀行的 branchTotal 在「分行」設讀鎖即隱含鎖住所有帳戶,而 deposit 只鎖一個帳戶餘額。

flowchart TD
  W[週] --> M[週一]
  W --> T[週二]
  M --> S1[時段 9到10]
  M --> S2[時段 10到11]
💡
關鍵

雙版本鎖像改草稿時別人仍能看正本、只在貼上牆那刻稍等;階層鎖像行事曆鎖整週或鎖單一時段。

STEP 3

實用超能力

階層鎖與意圖鎖怎麼運作

階層鎖 (hierarchic locks)

Gray 提出:用一個有不同粒度的鎖階層,設一個父鎖等同於設了所有對應的子鎖,省下大量鎖。在銀行例子裡,分行是父、帳戶是子。

每個節點都可被鎖,鎖一個節點=對它顯式存取、對其子節點隱式存取。

意圖鎖 (intention lock)

關鍵巧思:在對子節點上讀寫鎖前,要先在父節點及其祖先設一個「意圖讀/意圖寫」鎖,宣告「我等下要動底下某個子節點」。

  • 意圖鎖彼此相容(大家都只是宣告意圖,互不干擾)。
  • 但意圖鎖與真正的讀/寫鎖依常規衝突
已設 read write I-read I-write
OK OK OK OK
read OK wait OK wait
write wait wait wait wait
I-read OK wait OK OK
I-write wait wait OK OK

實戰:銀行如何避免衝突

  • branchTotal:在分行請求讀鎖 → 隱含對所有帳戶設讀鎖。
  • deposit:要對某帳戶餘額設寫鎖,但先在分行設『意圖寫』鎖

看表:分行已有 read 鎖時,I-write 請求要 wait。於是 branchTotal(分行讀鎖)和 deposit(分行意圖寫)不能同時跑,正確地序列化了——而不必對每個帳戶逐一上鎖比對。

階層鎖的好處:混合粒度時大幅減少鎖數量;代價是相容表與升級規則變複雜。長交易可鎖整批,短交易鎖細粒度,各取所需。

💡
關鍵

階層鎖設父鎖等同設所有子鎖;意圖鎖在祖先標記將讀寫子節點,使不同粒度操作能正確序列化又省鎖。

🔆
生活妙喻:雙版本鎖 ≈ 改草稿時別人仍能看正本

撰稿人改草稿(暫存版本)期間,他人照樣讀牆上的正本(已提交版本),只在草稿要貼上牆取代正本的提交那刻才需稍等,讀取被擋的時間極短。

🔆
生活妙喻:階層鎖與意圖鎖 ≈ 行事曆鎖整週或鎖單一時段

看整週就在週這層設讀鎖(隱含鎖住底下時段),填單一約會只鎖該時段;要動子節點前先在父層掛意圖鎖宣告,避免大小範圍操作互踩。

本節字彙

雙版本鎖 (two-version locking)
允許寫者寫暫存版本、讀者讀已提交版本,將互斥延到提交才用提交鎖的方案。
🧠 兩個版本:正在改的草稿與大家在看的正本。
階層鎖 (hierarchic locks)
以粒度階層組織鎖,設父鎖等同設所有子鎖,支援混合粒度以省鎖。
🧠 鎖父節點=一次罩住所有子節點。
意圖鎖 (intention lock)
在父節點宣告將對子節點讀或寫的鎖,彼此相容但與真正讀寫鎖衝突。
🧠 先在上層『打聲招呼』我等下要動底下。
雙版本鎖 (two-version locking) 相較於一般讀寫鎖,讀操作被延遲的時機有何不同?
在雙版本鎖中,交易要提交時協調者會怎麼做?
階層鎖 (hierarchic locks) 中,設一個父節點的鎖有什麼效果?
05

另類武器:樂觀並行控制與時戳排序

交易先自由進行,提交前才檢查是否衝突,並認識前向與後向驗證。

先讀原文段落,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 另類武器:樂觀並行控制與時戳排序 ging the client’s request to commit the transaction. Note that this timestamp ordering algorithm is a strict one – it ensures strict executions of transactions (see Section 16. The timestamp ordering read rule delays a transaction’s read operation on any object until all transactions that had previously written that object have committed or aborted. The arrangement to commit versions in order ensures that the execution of a transaction’s write operation on any object is delayed until all transactions that had previously written that object have committed or aborted.
白話導讀

交易先自由進行,提交前才檢查是否衝突,並認識前向與後向驗證。

樂觀並行控制:先做再驗證

交易先自由進行,提交前才檢查是否衝突,並認識前向與後向驗證。

STEP 1

深度探秘

假設衝突很少,先衝再說

為什麼要樂觀

Kung 與 Robinson 指出鎖的幾個缺點:

  • 維護鎖的開銷:連唯讀查詢都得上鎖,但其實大多時候沒人來搶。例如兩個程式各自更新 n 個物件,平均只有 1/n 的機會撞同一個——鎖往往只在最壞情況才需要。
  • 可能死結:預防死結會嚴重降低並行,偵測與逾時都不完美。
  • 為避免連鎖中止,鎖要握到交易結束,大幅降低並行。

樂觀法基於觀察:多數應用裡兩交易撞同物件的機率很低。於是讓交易盡情進行,假裝沒有衝突,等到要提交時才檢查。

三個階段

flowchart TD
  A[工作階段 用暫存版本自由讀寫 記錄讀集合與寫集合] --> B[驗證階段 收到closeTransaction時檢查是否與他人衝突]
  B --> C{驗證通過?}
  C -->|是| D[更新階段 把暫存版本變永久]
  C -->|否| E[衝突解決 中止某交易並重啟]
  • 工作階段 (working phase):每交易有自己的暫存版本,讀取讀已提交值或自己的暫存值,寫入只進自己的暫存值(別人看不到)。同時記錄讀集合 (read set)寫集合 (write set)。因為讀的都是已提交值,不會髒讀
  • 驗證階段 (validation phase):提交時檢查與其他交易是否衝突;不通過就要中止某方。
  • 更新階段 (update phase):通過後把暫存版本永久化。唯讀交易通過驗證即可立刻提交。
💡
關鍵

樂觀法假設衝突罕見,讓交易在工作階段自由用暫存版本讀寫,提交前才驗證,分工作、驗證、更新三階段。

STEP 2

生活妙喻

維基百科編輯與道歉比許可容易

像維基百科那樣「先編輯、送出時才看有沒有撞」

鎖是「事前許可」:動手前先搶到鎖,沒搶到就乾等。樂觀法是「事後驗證」:你儘管編輯(在自己的草稿上),按下送出時系統才檢查「這段期間有沒有別人也改了同一處」。

  • 沒撞(大多數情況)→ 順利送出,超有效率。
  • 撞了 → 顯示「編輯衝突」,請你重來。

背後的哲學是工程界名言:「請求原諒比請求許可容易」——既然衝突很少,何必每次都先去拿許可(上鎖)?先做,真出事再處理。

讀集合與寫集合:記下你「看過誰、改過誰」

驗證時要判斷會不會撞,靠的是每筆交易隨手記下的兩本帳:

  • 讀集合:我這趟讀過哪些物件。
  • 寫集合:我這趟改過哪些物件。

就像你編輯時記下「我參考了 A、B 段,動手改了 C 段」。送出時系統拿你的清單跟別人的清單比對,看有沒有重疊到會出事的地方。

💡
關鍵

樂觀法像維基百科先編輯、送出時才檢查衝突,奉行『請求原諒比請求許可容易』;靠讀集合與寫集合判斷是否相撞。

STEP 3

實用超能力

前向驗證 vs 後向驗證,與飢餓

驗證的兩種方向

交易進入驗證階段時拿到一個遞增的交易編號(像每完成一筆就跳一格的偽時鐘)。驗證就是檢查 Tv 與重疊交易 Ti 的衝突。

後向驗證 (backward validation)

拿正在驗證的交易的讀集合,去比對較早、已提交的重疊交易的寫集合。若重疊→驗證失敗。

valid = true;
for (Ti = startTn+1; Ti <= finishTn; Ti++)
  if (Tv 的讀集合 與 Ti 的寫集合 有交集) valid = false;
  • 唯一的解決方式:中止正在驗證的這筆(別人已提交無法動)。
  • 只有寫、沒有讀的交易不必檢查。
  • 需保留舊的寫集合,長交易時是負擔。

前向驗證 (forward validation)

拿正在驗證的交易的寫集合,去比對還在進行中(活躍)交易的讀集合

  • 較有彈性:可中止驗證中的這筆,或中止那些衝突的活躍交易,或延後驗證。
  • 唯讀交易因為沒有寫集合,永遠通過前向驗證。

比較與飢餓

面向 後向驗證 前向驗證
比對對象 大的讀集合 vs 舊寫集合 小的寫集合 vs 活躍讀集合
衝突解決 只能中止自己 多種選擇
成本 要保留舊寫集合 要應付驗證中又有新交易開始

飢餓 (starvation):靠中止重啟的方案無法保證某交易終究能通過——它可能每次重啟又撞上別人。Kung 與 Robinson 建議:若伺服器偵測到某交易被中止多次,就給它獨佔存取(用 semaphore 保護的臨界區)讓它做完。

💡
關鍵

後向驗證比讀集合與舊寫集合且只能中止自己;前向驗證比寫集合與活躍讀集合且較有彈性;多次被中止恐飢餓,需特別處理。

🔆
生活妙喻:樂觀並行控制 ≈ 維基百科先編輯送出才檢查衝突

不像鎖要事前拿許可,樂觀法讓你先在草稿上自由編輯,送出時才看這段期間有沒有人改同一處,奉行『請求原諒比請求許可容易』。

🔆
生活妙喻:讀集合與寫集合 ≈ 記下我參考了哪些段、改了哪些段

每筆交易隨手記錄讀過與改過的物件清單,驗證時拿來與別人的清單比對是否重疊到會出事的地方。

本節字彙

樂觀並行控制 (optimistic concurrency control)
假設衝突罕見,讓交易先自由進行,提交前才驗證是否衝突的方案。
🧠 樂觀=先衝再驗,相信大多不會撞。
讀集合/寫集合 (read set / write set)
交易記錄自己讀過、寫過哪些物件的兩份清單,供驗證階段比對衝突。
🧠 看過誰記讀集合,改過誰記寫集合。
飢餓 (starvation)
某交易因每次重啟都撞上衝突而永遠無法通過驗證提交的情況。
🧠 一直被中止重來,永遠輪不到它成功。
樂觀並行控制的基本假設是什麼?
在樂觀並行控制的工作階段,為什麼不會發生髒讀?
後向驗證 (backward validation) 比對的是什麼,且唯一的衝突解決方式是什麼?

時戳排序:用時間戳決定順序

每筆交易開始就拿到時戳,依時戳順序決定讀寫能否進行、延遲或中止。

STEP 1

深度探秘

一開始就決定誰先誰後

核心想法

時戳排序中,每筆交易一開始就拿到一個唯一的時戳 (timestamp),定義它在時間序列中的位置。所有請求都可依時戳全序排列。每個操作執行時就立即驗證——不行就立刻中止並可重啟。

基本規則非常簡單:

交易要某物件,只有當該物件最後是被更早的交易讀和寫時才有效。交易要某物件,只有當該物件最後是被更早的交易寫時才有效。

物件帶著哪些時戳

每個物件有:一個寫時戳 (write timestamp)、一組暫存版本(各帶自己的寫時戳)、一組讀時戳 (read timestamp)(可用最大值代表)。

  • 接受寫操作 → 建一個新暫存版本,寫時戳設為交易時戳。
  • 讀操作 → 導向「寫時戳小於交易時戳中最大的」那個版本。
  • 接受讀操作 → 把交易時戳加入該物件的讀時戳集合。
  • 提交時 → 暫存版本的值與時戳變成物件的正式值與時戳。

衝突規則(Tc 是當前交易,Ti 是別的交易)

規則 Tc 操作 Ti 操作 條件
1 write read Tc 不可寫已被更晚 Ti 讀過的物件(Tc ≥ 最大讀時戳)
2 write write Tc 不可寫已被更晚 Ti 寫過的物件(Tc > 已提交版本寫時戳)
3 read write Tc 不可讀已被更晚 Ti 寫過的物件(Tc > 已提交版本寫時戳)

重點:時戳排序的序列順序是靜態決定(交易開始時就定了),這跟鎖的動態順序(看存取先後)不同。

💡
關鍵

時戳排序在交易開始就發時戳定下全序,每個操作即時驗證,依物件的讀寫時戳判斷可否進行,否則立刻中止。

STEP 2

生活妙喻

領號碼牌的銀行櫃台

進門就領號碼牌

時戳就像你一進銀行就抽的號碼牌——號碼小的「資歷較深」,理應先被服務。整間銀行的服務順序在大家抽號的那一刻就靜態決定了。

相較之下,鎖比較像「沒有號碼牌,誰先擠到櫃台誰先辦」——順序是動態地看誰先到。

「來晚了」就被請回

時戳排序的規矩很硬:

  • 如果一個**號碼較大(較晚)的人已經先動過某筆資料,那麼一個號碼較小(較早)**的人現在才來想寫它——太遲了!按理它該排在前面卻姍姍來遲,於是它被中止、請它重新抽號(重啟)。

這就是「寫得太晚就中止」:因為一個時戳更晚的交易已經讀或寫過該物件了,這個遲到的早號交易若硬寫進去,會破壞「按號碼順序」的世界。

讀要等前面的人辦完

讀操作若指向一個還是暫存(未提交)的版本,就得做出那版本的交易提交或中止——像你要參考前一位客戶剛辦的業務,得等他正式辦妥。這種等待不會死結,因為大家只會等號碼更小(更早)的人,箭頭永遠朝同一方向,不會繞成圈。

💡
關鍵

時戳像進門就抽的號碼牌靜態定序;早號交易若寫得比晚號還遲就被請回重抽(中止),讀則等更早者辦完且不會死結。

STEP 3

實用超能力

寫規則、讀規則與多版本改良

時戳排序寫規則

對交易 Tc 寫物件 D:

if (Tc >= D 的最大讀時戳 && Tc > D 已提交版本的寫時戳)
    在 D 的暫存版本上執行寫 寫時戳設為 Tc
else
    中止交易 Tc   // 寫得太晚

「太晚」意思是已有時戳更晚的交易讀或寫過該物件。

時戳排序讀規則

對交易 Tc 讀物件 D:

if (Tc > D 已提交版本的寫時戳) {
    選出寫時戳 <= Tc 中最大的版本 Dselected
    if (Dselected 已提交) 讀它
    else 等待做出 Dselected 的交易提交或中止 然後重新套用讀規則
} else
    中止交易 Tc   // 讀得太晚
  • 讀到「太早」(指向未提交版本)→ 等先前交易完成(防髒讀)。
  • 讀到「太晚」→ 中止。

重要性質與改良

  • 這個演算法是嚴格的:讀寫都延遲到先前寫者提交或中止,達成嚴格執行。
  • 不會死結(只等更早者),但容易造成重啟
  • 忽略過時寫規則 (ignore obsolete write):若一個寫太晚但沒人讀過該物件,可直接忽略而不中止整筆交易(反正它的效果本來也會被蓋掉)。
  • 多版本時戳排序 (multiversion timestamp ordering):為每個物件保留一串舊的已提交版本。好處是讀永遠不必因為來晚而被拒——晚到的讀可以去讀一個舊的已提交版本。代價是儲存空間,但並行度高、無死結、讀操作總被允許。
💡
關鍵

寫規則與讀規則依讀寫時戳判定接受、等待或中止;演算法嚴格且無死結但易重啟,多版本法讓晚到的讀不必被拒。

🔆
生活妙喻:時戳 ≈ 進門就抽的號碼牌

交易開始即拿到號碼牌(時戳),整體服務順序在抽號那刻就靜態決定,號碼小者資歷深、理應先被服務,與鎖的『誰先擠到櫃台』動態定序不同。

🔆
生活妙喻:寫得太晚被中止 ≈ 早號客戶姍姍來遲被請回重抽

若號碼更晚的人已先動過資料,號碼較早卻遲到的交易硬要寫就會破壞順序,於是被中止、重新抽號(重啟)。

本節字彙

時戳 (timestamp)
交易開始時取得的唯一標記,定義它在時間序列中的位置,用來靜態決定序列順序。
🧠 進門抽的號碼牌,號碼定先後。
寫/讀時戳 (write / read timestamp)
物件記錄的最近寫入時戳與最大讀取時戳,用來判定操作可否進行。
🧠 物件記得『最後誰寫過、誰讀過』。
多版本時戳排序 (multiversion timestamp ordering)
為每物件保留多個舊已提交版本,使來晚的讀可讀舊版本而不必被拒。
🧠 留著歷史版本,晚到的讀也有得讀。
時戳排序如何決定交易的序列順序?
交易 Tc 想寫某物件,但該物件已被一個時戳更晚的交易讀過。依時戳排序寫規則會發生什麼?
為什麼時戳排序的等待不會造成死結?

三大方法比較與真實世界應用

比較鎖、樂觀法、時戳排序的取捨,以及 Dropbox、Google Docs 等如何用樂觀法。

STEP 1

深度探秘

三種武器的取捨

三種方法回顧

我們學了三種控制並行存取的方法:嚴格兩階段鎖樂觀法時戳排序。三者都有時間與空間開銷,也都會在某種程度上限制並行。

悲觀 vs 樂觀

  • 悲觀法 (pessimistic):鎖與時戳排序——存取每個物件時就當場偵測衝突
  • 樂觀法 (optimistic):讓所有交易先進行,提交時才檢查(前向驗證可更早中止)。衝突少時很有效率,但中止時得重做大量工作。

鎖 vs 時戳排序(都是悲觀法)

面向 時戳排序 兩階段鎖
序列順序何時定 靜態(交易開始) 動態(依存取順序)
衝突時的反應 立刻中止交易 讓交易等待(之後可能為解死結而中止)
擅長 讀多的交易(尤其多版本) 寫多的交易
flowchart TD
  A[偵測到衝突存取] --> B{用哪種方法}
  B -->|時戳排序| C[立刻中止]
  B -->|鎖| D[等待 必要時為解死結才中止]
  B -->|樂觀法| E[先放行 提交時才驗證再決定中止]

因此有人提出混合方案:讀多的交易用時戳排序、寫多的用鎖。歷史上分散式系統最主流的還是(如 CORBA 並行控制服務完全基於鎖,並提供階層鎖)。

💡
關鍵

鎖與時戳是悲觀法、樂觀法提交才驗;時戳靜態定序遇衝突即中止,鎖動態定序遇衝突先等待;讀多用時戳、寫多用鎖。

STEP 2

生活妙喻

三種餐廳帶位策略

用三種餐廳帶位法理解三種並行控制

  • 鎖=先到先佔桌,後到的乾等:你坐下就佔住桌子(上鎖),別人想用就得在旁邊等你離開。兩桌互佔對方想要的位子時,會卡死(死結)。適合「大家都要動很多桌」(寫多)的場合。

  • 時戳排序=進門領號碼牌,順序當場定:號碼決定一切。若你號碼小卻晚到、別人號碼大卻先動了,你就被請回重新領號(中止重啟)。對「只是來看看菜單」(讀多)的客人特別友善,尤其有多份菜單副本(多版本)時。

  • 樂觀法=先入座點餐,結帳時才查有沒有重複下單:大家先自由點,結帳時系統才檢查有沒有撞單。沒撞(多數情況)超順暢;撞了就請其中一桌重點(中止重做)。

為什麼鎖最常見

就像多數餐廳還是用最直覺的「先到先坐」——鎖在資料庫界用了數十年、最被理解、工具最成熟,所以分散式系統歷史上以鎖為主流。

💡
關鍵

鎖像先到先佔桌(寫多適用)、時戳像領號碼牌(讀多友善)、樂觀法像結帳才查撞單(衝突少最順);鎖因成熟而最常見。

STEP 3

實用超能力

雲端協作怎麼處理並行

三種排程選擇

排程一個操作時,永遠只有三種選擇:(1) 立刻執行、(2) 延遲、(3) 中止。三大方法只是用不同策略在這三者間取捨。

真實世界:雲端協作多用樂觀法 + 衝突解決

二十一世紀的網路協作應用,常用樂觀並行控制搭配衝突解決(而不是直接中止某一方):

服務 並行控制方式 粒度
Dropbox 樂觀法;兩人同改同檔,第一個寫入被接受、第二個被拒;提供版本歷史供手動合併 整個檔案
Google Docs 樂觀法;多人同編可即時看到彼此的游標與輸入,因持續感知而少衝突 文字逐字元、試算表逐儲存格(同格則最後寫入者贏)
Wikipedia 樂觀法;第一個寫入被接受,後者看到「編輯衝突」畫面請其解決 頁面
Dynamo(Amazon) 樂觀法加衝突解決;用 get/put 而非交易,不提供 ACID 的隔離保證 鍵值

共同點:既然衝突少,就先讓大家做,真撞上了再讓人合併或重做,而不是事先用鎖把大家擋住。這跟前面學的樂觀法精神一脈相承。

帶走的判斷力

下次設計系統時可以這樣想:

  • 寫密集、衝突多 → 偏向
  • 讀密集、衝突少 → 偏向時戳排序/多版本樂觀法
  • 使用者能彼此感知、可接受事後合併(協作編輯)→ 樂觀法 + 衝突解決

沒有萬靈丹,重點是看你的讀寫比例衝突機率選對工具。

💡
關鍵

排程只有立即、延遲、中止三選一;雲端協作多採樂觀法加衝突解決;選型看讀寫比例與衝突機率,沒有萬靈丹。

🔆
生活妙喻:三大並行控制方法 ≈ 三種餐廳帶位策略

鎖像先到先佔桌會卡死、適合寫多;時戳像領號碼牌、對讀多友善;樂觀法像結帳才查撞單、衝突少最順暢。

🔆
生活妙喻:雲端協作的樂觀法加衝突解決 ≈ 先讓大家自由編、撞了再合併

Dropbox、Google Docs、Wikipedia 都讓使用者先做,真衝突時提供版本歷史或編輯衝突畫面讓人合併或重做,而非事先用鎖擋住。

本節字彙

悲觀法 (pessimistic)
假設衝突會發生、在存取每個物件時即偵測衝突的方法,如鎖與時戳排序。
🧠 悲觀=先防著,每次存取都查衝突。
靜態 vs 動態定序
時戳排序在交易開始就定序(靜態),鎖則依存取先後決定(動態)。
🧠 號碼牌(靜態)一進門就定;佔桌(動態)看誰先到。
衝突解決 (conflict resolution)
發生衝突時讓使用者合併或重做,而非直接中止一方,常見於雲端協作。
🧠 撞了不一定要犧牲誰,可以合併或重做。
鎖與時戳排序在「偵測到衝突存取」時的反應有何根本差別?
依書中比較,哪種方法對「讀多」的交易較有利,哪種對「寫多」較有利?
排程一個交易操作時,永遠只有哪三種選擇?