交易可以提供什麼安全保證?與之相關的成本是什麼?
建構容錯 (fault-tolerant) 系統的最佳方式是以有用的保證來找到一些通用的抽象 (abstraction) 機制,實作一次然後讓應用程式可以依賴這些保證。例如,透過使用交易 (transaction),應用程式可以假裝不存在崩潰 (crash) (原子性)、沒有其他人同時存取 DB (隔離性)、儲存裝置完全可靠 (持久性)。即使發生 crash、競爭條件、和磁碟故障,交易抽象層隱藏了這些問題,因此應用程式不需要擔心它們。
舉例來說,在分散式系統裡,一個很重要的抽象是共識 (consensus),也就是讓所有節點在某件事上達成一致意見。有了共識的實作,應用程式可以將其用於各種目的。
比如在 single-leader replication DB,若 leader 失效需要容錯移轉到另一個節點,剩餘節點就可以利用共識來選出新 leader,而非兩個節點都認為自己是 leader (腦分裂) 而導致資料遺失。
Transaction#
在資料系統中,有很多事情可能出錯,例如:DB 軟硬體故障、APP crash、網路中斷切斷 APP 與 DB 的連接或 DB 節點間的連接、多個 client 同時寫入 DB 覆蓋別人的更新、client 間的競爭條件造成意外的 bug。系統為了要保持可靠性,必須處理這些故障並確保它們不會造成整個系統的災難性故障。要實現完善的容錯機制,有太多工作要做,幾十年來,交易 (transaction) 一直是簡化這些問題的首選機制。
交易是應用程式將多個讀寫操作組合成一個 logical unit 的方式。從概念上來講,交易中的所有讀寫都是作為一個 operation 來執行,要嘛整個交易成功 (commit),要嘛失敗 (abort, rollback)。也就是說,一個交易裡面包含多個對 DB 的操作,而這些操作要嘛全部都做,要嘛全部都不做 (all or nothing)。例如,當我們在實作一個轉帳功能 (A 轉帳 100 給 B),會需要對 DB 進行兩個操作:將 A 的餘額減掉 100、以及將 B 的餘額增加 100。當我們用交易來管理這整個轉帳流程,就能確保「扣掉 A 錢」和「增加 B 錢」這兩個操作會同時成功或同時失敗,即使轉帳過程發生問題,也不會造成 A 損失 100 而 B 卻沒收到之類的問題。
有了交易,應用程式的 error handling 就簡單多了,如果交易失敗,它可以安全地重試,不用擔心部分失敗的問題。交易被創造的目的是為了簡化應用程式存取 DB 的 programming model,使用交易讓應用程式可以忽略某些潛藏錯誤的情境和並發問題,因為它們都交給 DB 來處理,我們稱之為 安全保證 (safety guarantees)。交易提供的安全保證經常會用 ACID 來描述,這個詞被創造的目的是為了精確描述 DB 容錯機制,分別代表 原子性 (Atomicity)、一致性 (Consistency)、隔離性 (Isolation)、持久性 (Durability) 四個特性。如前述,交易的一個關鍵特性是,如果發現錯誤可以終止交易並安全地重試,而 ACID 資料庫就是基於這樣的哲學:如果 DB 有違反原子性、隔離性、持久性保證的危險,那它寧願完全放棄交易,也不允許有部分完成的狀況發生。
幾乎所有關聯式 DB 和一些 NoSQL DB 都支援交易和 ACID。但實際上不同 DB 的 ACID 實作並不相同,現在 ACID 基本上也變成一種行銷術語。not ACID 的系統有時稱為 BASE。另外,並不是每個應用程式都需要交易,有時為了獲得更好的性能 (比如交易吞吐量、查詢回應時間) 或高可用性,可能會刻意弱化交易的保障或乾脆不用,一些安全性考量也可以在沒有交易的情況下實現。
交易指的是組成一個邏輯單元的一組讀寫操作,它本身不一定要具有 ACID 屬性。
交易處理 (transaction processing) 僅意味著可以讓客戶端進行低延遲的讀寫,而批次處理 (batch processing) 只會定期運行,例如每天一次。
ACID#
嚴格來說, ACID 裡面只有 A, I, D 是資料庫屬性,C 是應用程式屬性。
Atomicity#
原子性:若寫入中途出現錯誤,就中止交易並丟棄該交易的所有寫入。
在 multi-threaded programming 中,若一個 thread 執行一個原子操作,表示另一個 thread 無法看到那個操作的中間 (half-finished) 結果,系統只能處於操作之前、或操作完成的狀態,而不能介於兩者之間。原子性描述的是,如果將寫入操作組合到一個原子交易中,當故障發生 (程序 crash、網路中斷、磁碟滿了等) 而無法完成 commit,交易就會中止 (abort),DB 必須 discard 或 undo 那個交易中的任何寫入。也就是說,交易不能被分割,DB 提供全有或全無 (all-or-nothing) 的保證,讓人不必擔心部分失敗的問題。可中止性 (abortability) 或許是更貼切的說法,若一個交易被中止,應用程式可以知道沒有東西被改到,所以可以安全重試。
Consistency#
一致性:交易執行的前後,資料庫皆處於某種良好狀態,數據的完整性是一致的。
這裡的一致性指的是對於資料的某些陳述 (不變量,invariant) 必須始終是正確的,比如在會計系統中,所有帳戶的借貸必須始終平衡。如果交易開始時 DB 是符合 invariant 描述的有效狀態,並且交易期間的寫入都能保持這個有效性,那麼交易後的結果也就依然能滿足 invariant 描述的狀態。然而這種一致性 仰賴於應用程式對 invariant 的認定,因為通常哪些資料有效、哪些資料無效是由應用程式定義,而 DB 只負責儲存,它無法阻止你寫入違反 invariant 的 bad data。所以應用程式應該負責正確地定義交易以保持一致性,也許會仰賴 DB 的原子性和隔離性來實現一致性,但這件事不是取決於 DB 而已。
Isolation#
隔離性:並發執行的交易是彼此隔離的,它們不能互相干擾。
當多個客戶端同時對 DB 的不同部分做讀寫,因爲彼此不互相依賴所以能安全的平行執行。但若他們同時存取相同資料,就會遇到並發 (concurrency) 問題 (競爭條件 (race condition)),比如兩個交易嘗試修改相同的資料、或一個交易要讀取的資料正在被另一個交易修改。

隔離性指的是並發執行的交易不應該互相干擾,即 DB 同時處理多個交易時,各個交易之間不能互相影響。例如,若一個交易包含了多次寫入,那另一個交易只能看到所有寫入操作完成 commit 的結果、或什麼寫入都沒看到,而不能看到中間的部分結果。DB 想要透過「交易隔離」來解決並發問題,讓並發交易的執行結果如同一個接一個照順序般,因此有了各種並發控制機制。
弱隔離級別 的機制,有基本的 read committed (防止 dirty read 和 dirty write)、利於分析和備份等唯獨查詢的快照隔離 (snapshot isolation) (防止 read skew 和簡單的 phantom)、原子寫入操作和應用程式顯示加鎖 (防止 lost update) 等;而 可序列化隔離級別 (防止 write skew 和前述所有競爭條件) 的實作方法,則有嚴格循序執行交易、兩階段加鎖 (2PL)、可序列化快照隔離 (serializable snapshot isolation, SSI)。由於可序列化隔離會犧牲性能,許多 DB 系統會採用較弱的隔離級別來防止一些 (並非全部) 的並發問題。沒有做好隔離導致 client 讀到髒資料,小則 email APP 未讀信件數量錯誤,大則影響銀行 APP 客戶帳戶餘額或轉帳交易。開發人員需要瞭解有哪些可能和不可能出現的競爭條件、以及防止做法,才能判斷應用程式到底需要什麼樣的隔離級別,而不是盲目仰賴工具。
MVCC: 快照隔離通常透過 MVCC 來實作。DB 必須保存物件的幾個不同提交版本,因為各種正在進行的交易可能需要查看 DB 在不同時間點的狀態。
由於同時維護了一個物件的多個版本,這種技術稱為多版本並發控制 (multi-version concurrency control, MVCC)。
Durability#
持久性:一旦交易 commit 成功,寫入的資料就不會消失,即使系統重啟或故障也不會丟失。
對於 single-node 資料庫,持久性通常表示資料已經被寫入非揮發性儲存設備 (硬碟或 SSD),通常還包括寫入像 B-tree 的 WAL (write-ahead log) 或類似的日誌,讓磁碟上的資料損壞時可以由系統進行復原。在 replicated 資料庫,持久性表示資料已經成功被 copy 到其他節點,DB 會等到寫入跟複製都完成才回報交易已經成功 commit。但實際上,沒有哪一種技術可以提供絕對完美的保證,我們只能用寫入磁碟、複製到遠端機器、備份等各種方法來降低風險。
*本篇內容對於 single-node 和分散式資料庫都適用。對於只在分散式系統出現的特殊挑戰,未來再集中討論。
Reply by Email
