1. 程式人生 > >Java架構-資料庫如何確保其操作被 100% 正確執行?

Java架構-資料庫如何確保其操作被 100% 正確執行?

我想你在使用資料庫的時候,心裡會假定這裡面的資料都是 100% 準確的。回想一下,你在工作中有沒有這樣做過:

  • 有人給你反映了一個問題,說資料錯了,你的自然反應是去檢查程式碼有沒有問題,而不會想到去確認資料庫有沒有問題?

  • 為了更快更方便地執行單元測試,你認為通過 Mock 資料加上斷言(assertion)來代替資料庫中實際儲存的資料是完全沒問題的。
    如果你這樣做過,或者有過這樣的看法,那你一定是在假定:資料都是 100% 準確的。

今天我們不妨來思考下,資料庫為什麼會使你有這樣的認知?是因為資料庫的開發團隊對其測試到位嗎?我想,真正起到決定性作用的是資料庫背後的設計理念 ACID,這就是我們今天的主題。

什麼是 ACID?

ACID 是原子性(Atomicity,或稱“不可分割性”)、一致性(Consistency)、隔離性(Isolation,又稱“獨立性”)、永續性(Durability)的首字母簡稱。Andreas Reuter 和 TheoHärder 這兩位前輩在 1983 年提出它,指出一個數據庫“事務”只要滿足這 4 個特性,在任何情況下資料都能保證準確。

“事務”是資料庫的執行單元,除了我們平時用顯式宣告的 transaction 之類關鍵字包裹的程式碼外,每一條單獨的 SQL,也是以事務的形式執行的。比如,當你在一條 SQL 中同時 insert 多筆資料的時候,一旦發生異常,所有的這幾筆資料最終都不會被插入到目標表中,會一併撤銷。

在保證達到這個效果的過程中,ACID 的四個特性分別起到了什麼作用呢?

1. 原子性

一句話來概括原子性,用於保證每個事務被視為單個完整的個體,不可分割。滿足原子性的事務,要麼完全成功,要麼完全失敗,不允許存在其他中間狀態。通常這點指的是我們同時執行多條 SQL 語句的時候,可以將這些 SQL 語句的生效與否捆綁到一起,以保證最終要麼全部資料被更新到資料庫,要麼全部都不更新到資料庫。我們來看一個例子。

小明讓小王代購了一些東西回來,需要在微信上支付給他 1000 元。當小明輸入完金額點選“確認轉賬”之後,執行的 SQL 至少是這樣的:

update balance = balance - 1000 from account where id = '小明的 id'``
update balance = balance + 1000 from account where id = '小王的 id'

注意,這兩條語句中只要任意一條執行失敗,而另外一條執行成功,那麼從原子性的要求來說,所有執行成功的修改都需要一併撤銷,恢復到最初的狀態,這個撤銷操作我們稱為“回滾”。否則,微信體系中的總餘額會無故多出或少了 1000 元。

資料庫中原子性的主流實現方案是通過日誌來做的,每一次操作資料前都會先將當前資料記錄到日誌中,這樣在需要回滾時,我們只要把 Undo Log 中的資料拿出來還原,就可以撤銷已經執行成功的操作。

原子性是四個特性中最核心的一個,僅關注當前的這一次操作,不考慮是否存在其它的什麼操作。

2. 隔離性

在上面小明和小王的故事中,如果再出現一個人小張,他也讓小王代購了東西要付錢,會出現新的情況,如下圖。

注意一下紅字部分。我們發現,這個時候哪怕兩次轉賬的事務分別保證了原子性,並且執行成功,最終的結果還是有可能出錯。

上圖中的現象,我們稱為“丟失更新”(Lost Update)。當然,還有其他可能產生的現象,比如髒讀、不可重複讀、幻讀,等等。不過,我們暫時不需要過多糾結於這些現象,你只要記得:當僅滿足原子性的前提下,如果遇到併發執行,依舊會出現資料錯誤。

所以,這時候我們需要通過隔離性的指導來避免這些問題。隔離性本質上指導解決的是一個資源競爭問題,通俗點說,就是多個事務併發執行後的狀態,應該和它們序列執行後的狀態是一致的。

在資料庫中解決資源競爭問題與其它軟體系統無異,就用鎖。在資料庫中對鎖的運用不同,因此產生了不同的隔離級別,不同的隔離級別對應解決的是前面提到的這些異常現象。如,讀未提交(Read Uncommitted)解決了丟失更新,讀已提交(Read committed)多解決了髒讀,可重複讀(Repeatable Read)又多解決了不可重複讀問題,最高級別的可序列化(Serializable)解決了全部這 4 個問題,即丟失更新、髒讀、不可重複度、幻讀。

其實在實際的運用中,遇到的場景會更復雜,所以詹姆士·格雷(Jim Gray)等人在 1995 發表了論文“對 ANSI SQL 隔離級別的批評(A Critique of ANSI SQL Isolation Levels)”將上表做了擴充,增加了遊標穩定(Cursor Stability)和快照隔離(Snapshot Isolation)隔離級別,指導我們在做隔離時,可以為獲得更好的效能進行一些新的嘗試。

3. 永續性

當你使用一些雲產品寫文章的時候,洋洋灑灑寫了幾千字,安心睡覺去了,第二天起來發現內容停留在剛起筆的那幾個字。任何的資料變更完成之後,就相當於成為了“歷史”,需要儲存下來才能為未來所用。因此資料庫需要具備永續性,才能為我們所依賴用於儲存資料。

如今,我們幾乎都是利用硬碟作為資料庫的儲存介質,來保證永續性。那麼理論上,除非硬碟本身故障,否則都不應該出現這樣一種情況:一條 SQL 變更成功後,發生資料丟失或者資料回到更早的狀態。

另外,因為所依賴的儲存介質本身也可能出現故障,所以我們可以通過將相同資料冗餘儲存在多個儲存介質上,並同時提供讀寫服務。冗餘越多,越無限接近 100% 的永續性效果。

4. 一致性

一致性的含義其實很簡單,就是最終結果的對與錯,是否是你所希望的結果。任何系統如果無法確保產生的資料結果與預期一致,那麼整個系統其實是沒有價值的。

回到前面小明和小王的例子。只要小明賬戶少了 1000 元,小王賬戶必須要多出 1000 元,這才是我們所希望的結果,否則都是錯的,也就是“不一致”的。

那麼你可能會問,這麼一說,一致性和原子性意思好像差不多啊?關於這點你可以這樣來理解:

  • 原子性關注的是關係和過程,確保指定的 SQL 之間是一個命運共同體;比如雞蛋孵小雞這個過程,必然是雞蛋破了後小雞再出來,而不是雞蛋破了,小雞不見了或者雞蛋沒破,不知道從哪哪冒出來個小雞。

  • 而一致性關注的是結果,這個結果的預期是你來定的,如何達到這個結果的過程並不是它所包含的概念。還是雞蛋孵小雞這個事,比如你預期一個雞蛋裡只能孵出一個小雞,那麼如果最終 9 個雞蛋裡出現了 10 個小雞,這時就是不一致的。

由於一致性只表示一個結果,它只是指引出一個正確的工作方向。而要達到這個正確的結果並不完全是由資料庫保證的,它只是一個按規則辦事的“監督者”。但是,它提供了主鍵、外來鍵、約束、欄位型別等,讓你可以在不同層面上定義什麼是“一致”。一旦不符合你的定義,資料庫就會丟擲異常來提醒你,這裡不符合你的預期了。

清楚了這 4 個概念,我想有必要將它們聯絡起來,看清它們之間的依存關係,以對四個特性有一個整體的認識。

ACID 間的聯絡

在我的理解中,原子性(A)、隔離性(I)、永續性(D)是為達到一致性(C)而存在的。

可以理解為,只要滿足了原子性(A)、隔離性(I)、永續性(D)那麼資料儲存層面的一致性(C)自然也就滿足了。

不過,站在一個完整的系統角度來說,要達到真正的一致性,還需要我們在 Coding 的時候有意識的去定義達到“正確結果”的程式碼邏輯。

為什麼要聊 ACID?

聊 ACID 的原因是分散式系統中的一個經典定理——CAP。CAP 是指導我們進行多程序之間互動的設計理論,告訴我們該如何去權衡一致性(C)、可用性(A)、分割槽容錯性(P),這也是它這三個字母所表達的含義。

我想,如果你知道分散式系統理論中的 CAP 定理,肯定會好奇 ACID 和 CAP 兩者定義的“一致性”表示的是不是同一個意思。其實不是:

  • 描述的主體不同,ACID 中的 C 指的是資料庫事務的一致性,而 CAP 中的 C 指的是程式之間請求的一致性。

  • 對結果的定義也有些差異,CAP 中的 C 除了一致性之外還帶著一些原子性的意思,一次操作中產生的多個請求要被視為一個完整的個體,不可分割,這個特點和資料庫中的原子性是一致的。

所以你會發現,CAP 定理中所表述的“一個請求”類似於資料庫 ACID 中的“一條 SQL”,並且還保留了原子性和一致性的含義。然後基於分散式的場景,衍生了分割槽容錯性以及可用性的概念。

CAP 定理作為後來者,為分散式系統而生,是分散式系統設計的指導方針。理解了 ACID,更有利於你去理解 CAP。

總結

工作中,我們參與開發的系統大部分都需要承載各自的業務,而這些業務就是在處理過去發生以及正在發生的事件。那麼,如何確保這個過程中產生的資料能被準確無誤地儲存下來,是我們要格外重視的部分。因此,在運用資料庫的時候,我們不單單要知其然,還要知其所以然。

文中我還提到了很現實的一點,整個系統層面的一致性無法單方面地依賴資料庫來滿足。因為什麼樣的結果被認為是一致的,從源頭上還需要你通過程式碼來定義。既然如此,建議你儘量通過上層程式的 Coding 來做一致性相關的校驗。這樣的做法會更有利於在做了負載均衡的分散式系統中,具備更好的伸縮性。好的伸縮性意味著一個系統可以根據流量的大小靈活增加或減少節點。從這個角度來說,應用程式由於更多承擔的是邏輯運算,不需要儲存資料,相比資料庫,更容易去“伸縮”。另外,減少對資料庫一致性校驗的依賴,也可以大大緩解資料庫所在主機的 CPU 壓力,使得運用單體資料庫的瓶頸來得更晚。

你在工作或者學習中,是否有遇到過資料異常的場景呢?是由於什麼原因導致的呢?歡迎在下方評論區留言

我本人邀約各大BATJ架構大牛共創Java架構師社群群,(群號:673043639)致力於免費提供Java架構行業交流平臺,通過這個平臺讓大家相互學習成長,提高技術,讓自己的水平進階一個檔次,成功通往Java架構技術大牛或架構師發展。

為什麼某些人會一直比你優秀,是因為他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜!

合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

希望此文能幫到大家的同時,也聽聽大家的觀點。歡迎留言討論,加關注,分享你的高見!持續更新

  • To-陌霖Java架構

分享網際網路最新文章 關注網際網路最新發展