Designing Data-Intensive Applications 讀後心得 (2)

fcamel
fcamel的程式開發心得
7 min readMar 1, 2020

上篇提到分散式系統的基本概念 (repllications、partitioning),這篇摘要書中介紹 transactions,著重在單一節點的 transactions。

Transactions 要解決什麼問題?提供什麼保證?

若有一系列相關的操作,其中有部份寫入失敗,會導致資料狀態不明確,很難處理。Databases 提供 transactions 保證整批操作會全部一起生效或全部無效,這大幅減輕應用程式的負擔。

Transactions 提供四項保證 (ACID) 解決問題:

  • Atomicity: 即整批寫入全部一起生效或無效。作者認為改叫 abortability, 語意會比較明確。
  • Consistency: 作者認為語意不明,湊字數的。
  • Isolation: 每個 transaction 互不影響,提升效率。
  • Durability: 資料「不會」掉。單機通常指有用 write-ahead log。多台機器是指有同步資料到其它台。

要作到最強的保證,就是讓所有 transactions 錯開照順序執行,但效率很差。普遍的作法是提供較弱的保證,但保有不錯的效率。這其實和 CPU 提供給 multi threads 的保證很像。為了效率,要求應用程式多留意一些 race condition 問題,OS 提供必要工具 (lock、atomic operations、memory barriers 等) 避免 race condition。

後面挑幾個重點講。

避免 Dirty Reads、Dirty Writes、Read skew

節錄書中定義:

* Dirty reads: One client reads another client’s writes before they have been committed.

* Dirty writes: One client overwrites data that another client has written, but not yet committed.

* Read skew (nonrepeatable reads): A client sees different parts of the database at different points in time.

下圖是 Read skew 的示意圖:

書中附圖

普遍解法是用 snapshot isolation,又稱為 Multi-Version Concurrency Control (MVCC),概念如下圖:

書中附圖

也就是每個 transaction 自成一個 snapshot,這樣執行當下不會互相影響。實作方式是每個 object 都加上版號,同時保留多個版本 object。事後再用 garbage collection 清掉沒在用的版本。用 index 查詢時,可以先查出同物件全部版本,再濾掉不合的版本。

這個作法對長時間 read query 很有幫助,比方說要 dump 整個 table,不用擔心會讀到不一致的資料,也不會影響 write query 的效能。

避免 Lost Updates、Write Skew、Phantom Reads

節錄書中定義:

Lost updates: Two clients concurrently perform a read-modify-write cycle. One overwrites the other’s write without incorporating its changes, so data is lost.

Write skew: A transaction reads something, makes a decision based on the value it saw, and writes the decision to the database. However, by the time the write is made, the premise of the decision is no longer true.

Phantom reads: A transaction reads objects that match some search condition. Another client makes a write that affects the results of that search.

下圖是 write skew 的例子:

書中附圖

上圖的情境是: 醫院要求至少有一人值班,Alice 和 Bob 同時請假,導致兩組操作執行時都查到還有兩人值班,然後同時請假,導致無人值班。注意,這個例子裡,兩個 transactions 並沒有同時更新到同一個物件。

即使有 snapshot isolation 也無法防止上述問題 (有些可防止 lost update)。解決這些的方法和 multi-threading 解法很像。像 lost updates 可用 atomic opeartion / compare-and-set 解,或是用 lock。但像 write skew 只能用 serializable isolation 解。

serializable isolation 是指 transactions 執行起來像是依序執行一般,有點像 multi-threading memory model 的 sequential consistency

有三種不同實作 serializable isolation 的方法:

  • 真的照順序一個一個執行 transaction: 缺點是很慢,無法善用多 CPU 效果。
  • Two-phase locking (2PL): 其實就是 multi-threading 的 RWLock。避免會有 write conflict 的操作。
  • 相較於 2PL 的「悲觀鎖」 (pessimistic locking),改用樂觀鎖 (optimistic locking)。例如 serializable snaopshot isolation (SSI)。先作再說,作完發現有問題就 abort 再重試。

2PL 是過去的主流作法,SSI 比較新。在 contention 低的情況,SSI 理論上會有較佳的表現。書上有介紹一些 2PL 和 SSI 細節,像是 databases 如何從 SQL 中判斷 2PL 要鎖那些物件?SSI 要怎麼追踪可能互有影響的 transactions?這裡略過不提,知道多數單機 databases 有提供這些保證,可以放心使用即可。

結語

單機 database transactions 和 multi-threading programming 很像,用的方法也差不多,只是 database 提供抽象的語法讓開發者比較不用擔心 race conditions。像 2PL 有 deadlock 時 databases 會自動 abort 其中一個 transaction,自己寫程式有 deadlock 就沒那麼好命了。

在分散式系統裡情況大不相同,之後再提書中建議的作法。

相關文章

--

--