RDBMS (關聯式資料庫) - ACID 基礎觀念

絕大多數的系統採用的一定是關聯式資料庫,而不是所謂的 NoSQL,應該是 NoSQL 的存在是為了一些大型系統的應用場景,或是輔助關聯式資料庫之用的。

其實在我開發 Backend 的經驗,我都使用過 RDBMS 及 NoSQL 過,我不認為 NoSQL 在開發速度上有比 RDBMS 快,其中如果系統裡面的資料有許多 Join 的情況,使用 NoSQL 根本就是自討苦吃,更何況自己開發的專案只是一個小系統,又沒有什麼超高流量,初期的小系統就應該使用 RDBMS。

如果是說創 Table 及定義欄位的方便性,如果用 RDBMS 覺得開發慢,其實我覺得百分之八九十是因為當前系統的商業邏輯沒想好及基本功不好,才會影響開發速度。

此外,現今最夯的 NoSQL----MongoDB,我有過損失資料及資料不正確的經驗,可想而知當前的 NoSQL 對於資料流失還沒有一定程度的保證,如果用 NoSQL 一定是系統對於資料容忍度高加上超高流量才會考慮 NoSQL。

以下的內容是參考:https://github.com/TritonHo/slides

這位大佬的文章真的很厲害,我拜讀已久,最近要努力整理對於他文章的筆記,小弟我有去聽過他開的淺談受益良多 QQ

使用 RDBMS 優點

  1. 大部分主流的 RDBMS 開發歷史悠久,許多錯誤也經過不斷的修正,值得信賴
  2. 對於資料容忍度低的系統,使用 RDBMS 是不二的選擇,因為有其 ACID 特性,也是今天要特別講的!
  3. RDBMS 支援多種資料型態,對於要產出報表的系統使用 RDBMS 是最理想的
  4. SQL 語法很強大,絕大多數的商業計算可以用 SQL 語法就算出,不需要拿到 app layer 來計算
  5. 一經 Commit 成功,其更改成功的資料必定存在,除非硬碟受損,不然資料一定都在!(所以還是要記得備份啊!)

ACID 基礎概念介紹

原子性 (Atomicity)

是指在一個 Transaction 裡面,可以包含多個 SQL 指令,也就是會有多個步驟,這些步驟一定是按照順序依序執行,如果其中一個步驟失敗,則整個交易宣告失敗,而交易中已經執行過的步驟如果是涉及到資料改動,則需要全數 Rollback 回去,回到還沒開啟 Transaction 前的狀態。

注意:

  1. 只要沒有成功 Commit,就算當機了或是其他原因,還沒 Commit 的 Transaction 一定會被 Rollback
  2. 當機後,如果需要修復資料,RDBMS 必須以 Transaction 為單位進行修復

為什麼要注意這些,因為你不能保證再做資料更改的當下,是否裝有資料庫的伺服器會不會遇上不可預期的錯誤,導致伺服器關閉,那麼當你重啟伺服器及資料庫的時候,資料庫的資料必須要是正確的,不能有數據錯誤的問題產生

用戶轉帳舉例

一個用戶 A 要匯錢給一個用戶 B,這就是一個 Transaction,所以通常這個 Transaction 裡面會有兩個 Update Statement:

1
2
3
4
START TRANSACTION;
Update user set balance = balance - amount where username = 'UserA' and balance >= amount;
Update user set balance = balance + amount where username = 'UserB'
COMMIT;

假設沒有原子性,那麼當第一句的 Update 成功,而第二句的 Update 失敗,整個交易結束。會導致 UserA 錢減少了,UserB 的錢卻沒有相對應的增加。

因此原子性保證 Transaction 裡面的每個 SQL statement 一定會執行,如果有一個執行失敗就會當作交易失敗,其他執行成功的全數 Rollback,反之如果都成功,並 Commit 成功,則交易成立,因此可以保證資料庫的數據能夠從一個正確狀態直接移到下一個正確狀態

簡短的兩個 Statement 就能安全地解決用戶轉帳的問題,但:

如果這樣的操作要在 NoSQL 完成,是需要 9 個步驟的 2 phase commit,大幅提升了難度及出錯的可能。

一致性 (Consistency)

指的是交易裡面進行資料改動,如果這筆交易是成功的,則裡面的資料改動必須滿足 unique constraint 和用戶自定義 constraint,例如一些 FK 跟資料欄位型態的限制,這些都要保證遵守。也代表此筆交易才能 Commit 成功,否則只要不遵守,就應該當作交易失敗,裡面進行的資料改動需要全數 Rollback。

注意:

  1. 當我們在寫程式的時候,遇到任何 Statement 執行錯誤的話,只要執行一句 Transaction Rollback 即可。不用考慮到底要怎麼 Rollback,資料庫會自動幫我們處理到好!
  2. 在 NoSQL 的 2pc,需要花大量的精力來處理 Rollback 的問題!
  3. 因此原子性跟一致性其實是相對應的概念,原子性保證數據可以安全移到下一個狀態,一致性保證數據移動失敗的時候,可以安全地回到本來正確狀態

所以,RDBMS 可以保證用戶轉帳成功,對方有成功收到錢,自己也確實扣到錢,在搶票中,可以保證明確付了錢,也確實搶到票,而不會有付了錢卻搶不到票的問題。

隔離性 (Isolation)

指的是同一筆資料,保障不會被兩個 Transaction 同時更改,導致 Race Condition 的狀況!Isolation 有分好幾種等級,在最低等級是會導致 Race Condition,最高等級不會有 Race Condition 情況,但是其效能會大幅度降低,也就是說越低的資料容忍度的系統其效能一定不會太好,因為資料庫這邊需要做嚴格的隔離,才不會有 Race Condition 的情況導致資料不正確。

RDBMS 好處就在於,要避免 Race Condition,一定會有其 LOCK 機制,而 RDBMS 可以幫我們管理 LOCK 機制,並不用我們這些開發者去實現,這個難度極高 QQ

注意:

  1. 如果沒有 Isolation,用戶是可以在同秒在多個 ATM 做提錢的動作,而這樣的話會導致用戶多次成功領到錢,而用戶的帳戶卻沒有按照成功的次數明確扣款!就是搶錢一波辣!

  2. 以搶票的例子來講的話,今天 AB 同時來搶同一個座位,而沒有 Isolation 的情況下,有可能 A 付錢成功了卻沒有拿到座位,而 B 付錢成功並拿到了座位,公司反而多賺了一筆錢,A 大虧!

  3. 來看一個用戶提款例子:

    1
    2
    3
    START TRANSACTION;
    Update user set balance = balance - amount where username = 'UserA' and balance >= amount;
    COMMIT;

    如果有 Isolation 的 LOCK 機制,當執行 Update Statement 時,RDBMS 會為 update 會影響的數據進行 Write Lock,也就是該 UserA 的帳戶被凍結一樣,其他人不能對 UserA 的帳戶進行匯款或提款等動作。

    直到這個 Transaction 結束後才會將 Write Lock 進行釋放,來保證同一個資料不會被同時改動。

    其中,可以注意的是 RDBMS 支持 atomic check-and-set 模式,可以在同一句 Update Statement 下同時檢查用戶是否有足夠錢可以提款,如果有足夠錢才會進行用戶扣款的更新。

  4. SELECT … FOR UPDATE 的重要性

    今天如果情境是使用者購買機票,則商業邏輯應該如下:

    1. 拿取當前座位數量
    2. 檢查座位數量是否為 0,如果為 0 則 return 賣光了
    3. 如果不為 0,則檢查使用者的帳戶是否足夠錢來購買座位
    4. 有的話就扣款,沒有的話則 return 用戶餘額不足
    5. 成功扣款並建立訂購紀錄,成功拿到座位
    6. Commit

    今天如果兩個人同時訂票,如果你 Select 座位數量的時候是有座位的話,理論上就會進行購票的動作,但如果另外一個人 Select 座位數量也是有座位,可是今天別人先完成交易,但是我也完成交易,別人拿到座位,我沒拿到座位。

    這就是 Race Condition!

    透過 Select … For Update 的語法將機票座位的 record 進行 lock,讓 UserA 先完成交易,UserB 只能等 UserA 完成交易後,在得知座位數量,在做交易。

    下一篇文章會提供這個實作例子,才不會講得很模糊~~

持續性 (Durability)

是只當交易一旦 Commited 成功,除非存放空間的硬體受損,否則資料永不流失。即使在資料寫入得當下當機,引發寫入的資料流失,RDBMS 也有機制在之後復原資料。

大部分的商用系統對於資料應該是低容忍度的,所以數據錯誤及流失是不被允許的,更何況是銀行的那些金融產業,如果他們沒有好的 RDBMS 的設計,今天你的錢就會時而多時而少。

所以有時候覺得一些帳戶操作的系統跑得很慢,為什麼?因為需要高度隔離,這一定會導致效能降低,今天要設計出一個系統是 High performance + high concurrency + high durability,這三項同時滿足的系統是很難很難達到的!

總結

之前對於 NoSQL 感到很新奇,也覺得一開始開發上就像遇到了天堂依樣,但是當我吃到苦頭後,我就再也不覺得 NoSQL 很好用了。

現在我開發 Backend,我不會一股腦地想要用 NoSQL,而是仔細思考系統的性質來決定用 RDBMS 還是 NoSQL,但是絕大數的情況,初期開發的系統絕非是大系統,因此選擇 RDBMS 開發才是正解,當系統發展到程度居然要用 NoSQL 才能緩解,那麼那時候系統一定很成功,絕對有那個開發時間跟錢跟精力來重構系統的~~~

之後會發一系列有關 RDBMS 跟 ORM 的文章,這些都很基礎,是操作資料庫都要知道的知識,之前的我太依賴 ORM 了,現在吃了苦頭要用 RAW SQL 的時候才知道基礎的重要性,ORM 很好用但絕對要知道其包裝底下的 SQL 語句到底長怎樣,這才是重要的,否則當系統很慢出在資料庫身上時,就會很難找到答案的。