說到數據庫,以前我老師有一句很經典的話。你可以不會寫SQL,但是一定不能不知道ACID。
在工業領域,SQL可以說是應用最廣泛的技術。從后端到算法,從數據到DBA,再到產品,甚至連一些運營也會基本的SQL。所以如果你現在還不太會的話,我建議你用一個下午的時間找個網站好好學一下。
原本我是想直接寫些Hbase相關的內容,但是我發現要想講清楚Hbase,必須要講noSQL數據庫。如果將noSQL,則又離不開最傳統的關系型數據庫。所以我們一步一步來,先從基礎的關系型數據庫講起。或許我這么說并不準確,因為數據庫并不基礎,相反它十分復雜。從索引到各種優化和設計原理,再到內部的各種算法和數據結構,涉及到的內容非常多。我們先把浩如煙海的知識放一放,先從最核心的數據庫四大原則開始說起。
數據庫事務ACID四大原則,A代表Atomicity,即原子性。C表示Consistency,即一致性。I表示Isolation,即隔離性。D表示Durability,即持久性。
這四個原則了解過數據庫的應該都如雷貫耳。可是真正面試的時候被問起來,能一個不落說得上來,并且講得清楚原委的就不多了。我覺得主要是因為我們的翻譯過于文雅,不像英文那么直觀,所以很難顧名思義。另一個原因是我們在學習的時候理解不夠深入,只知道原因,不知道原因的究竟。所謂知其然,不知其所以然。
原子性
讓我們先從其中最簡單的原子性開始。
原子性理解起來最簡單,也最常用。我就在面試當中遇見過不止一次,還有一次讓我用JAVA寫一個轉賬的功能,其實就是想看看我知不知道原子性。
原子兩個字看起來一頭霧水,其實這里不是指物理學上的基本粒子,而是指的不可分割的意思。也就是說在一個事務當中的所有操作應該被視為一個不可分割的整體,要么全成功,要么全部失敗。這點用轉賬這個問題舉例最合適。A將銀行卡里的錢轉100給B,很明顯,數據庫需要做兩件事情,一件事A賬戶扣款100,另一件是B賬戶收入100。但問題來了,計算機系統并不是100%可靠的,可能會存在極小的可能失敗。如果A扣款之后,發生網絡延遲或者系統down機,導致B賬戶的錢沒有增加,那怎么辦?A不是白白扣了錢?
A白白扣了錢是小事,一個金融系統如此不穩定,顯然是不能接受的。所以,在數據庫的事務當中,應該保證原子性。扣錢和收錢雖然是兩個操作,但是應該被視為一個。要么一起成功,要么一起失敗。失敗了還可以重試,如果成功了一半,那都不知道該怎么修復了。
事務的一種實現方法是在執行的時候先不將最終的結果更新到數據庫,而是先寫在事務日志上。等整個事務執行成功之后,再將事務日志上的內容同步到數據庫當中。如果失敗了,則將事務日志刪除,完成回滾。
持久性
第二個要介紹的是持久性。
持久性指的是數據的持久性,指的是事務完成了之后,這個事務對數據庫所作出的修改就被持久地保存進了數據庫當中,不會再被回滾操作影響。即使出現了各種事故,比如機房斷電、網絡故障等等意外情況,數據庫當中的數據也不能丟失。
但是前文當中說了,計算機系統很難做到100%可靠。如果萬一的情況發生了,數據庫當中的數據丟了,那么應該怎么辦呢?
沒關系,之前在介紹原子性的時候介紹過了。所有的事務操作在執行之前,都會先把數據記錄到事務日志當中,再同步到數據庫。即使是數據庫里的數據丟失了,那么只要根據事務日志重新執行一遍對應的操作,就可以恢復數據庫當中的數據,維持數據庫的持久性。實際上,現在的數據庫默認會將所有的操作都當做事務來執行,因此基本上不用擔心數據丟失的情況。
隔離性
然后,介紹的是隔離性。
在我們理解了原子性之后,隔離性就很好理解了。當我們同時有多個事務一起執行的時候,如果隔離性做得不好,很有可能導致很多問題。
以下四種問題最常見:
1. 臟讀
臟讀是指一個事務讀到了另一個事務執行的中間結果。還用我們剛才的轉賬的例子舉例:
當我們轉賬的事務沒有執行完,另一個事務就讀取了它的中間結果,很有可能就造成臟讀。因為萬一之前的事務回滾,那么新讀取到的結果就是錯的,和A賬號回滾之后的余額不一致。如果這個數據應用在其他的系統當中,就會引起大規模的數據問題。
2. 不可重復讀
不可重復讀的意思是說,如果在一個事務當中,我們讀取了某個數據兩次。剛好在這中間,有另一個事務修改了這條數據,那么同樣會引起數據錯誤,因為這兩次讀取到的結果不一致。
比如我們對A賬戶的一個事務還沒有結束,這時候它的結果就被其他事務修改了。那么程序就會發生錯亂,因為讀到了它沒有預料到的修改。
解決方法是針對當前修改的數據進行隔離,同一時刻只允許一個事務對該條數據進行修改,以保證數據的一致性。
3. 幻讀
幻讀的概念也很簡單,就是一個事務讀取兩次,讀到的數據條數不一致。這點和不可重復讀非常類似,不過不同的是不可重復讀針對的是確定的某一條數據,而幻讀指的是對整個數據庫或者是整個表而言。
要解決也很簡單,因為幻讀是其他事務修改新增或者修改其他數據產生的,所以要排除掉這種情況,只針對我們修改的數據進行加鎖和隔離是不夠的。我們需要將整個數據庫,或者是分區進行隔離,同一時刻,只允許一個事務對一個分片或者是數據表進行修改。
4. 更新丟失
更新丟失的定義很直觀,當我們針對一條數據進行修改的時候。同時也有另一個事務在修改同一條內容,會導致后者覆蓋前者的內容。比如說賬戶里原本100元,A事務往賬戶里添加10元,B事務往賬戶里扣除20元。A修改成110的同時,被B事務的80所覆蓋,導致A的操作就像是沒有執行過一樣,引起更新丟失。這個問題在并發場景當中也最為經典。
解決的辦法同樣是做好隔離操作,在一個寫入完成之前,禁止其他事務的讀入。事實上更新丟失是并發場景下最容易出現的錯誤,而且如果設計不合理,出現了錯誤也會非常難排查。
數據庫解決隔離性問題的辦法就是設置不同的隔離級別,不同的隔離級別對應不同的隔離策略,可以保證不同級別下的隔離性。不同的隔離級別意味著使用不同級別的鎖,顯然隔離級別越高意味著性能越差。所以這就需要數據庫管理員(DBA)對于當前的應用場景,以及并發量和數據風險有一個非常清楚的認知。能夠在性能和安全性之間做一個權衡。這里,我們不多做具體的探究,觀察一下下圖,簡單了解一下即可:
從上到下以此是四種隔離級別,越往下隔離級別越高,能夠解決的隔離性問題也就越多。同樣的,用到的鎖也就越多,系統的性能也就越差。
最上面未提交讀是最低的隔離級別,在讀取的時候并不會判斷是否可能會讀取到沒有提交的數據。所以它的隔離性最差,連最簡單的臟讀都無法解決。
已提交讀則是通過鎖限制了只會讀取已經提交的數據,讀數據的時候使用的共享鎖,在讀取完成之后立即釋放。這種隔離級別只能夠解決最常見的臟讀問題,它也是SQL server數據庫的默認隔離級別。
可重復讀的讀取過程和已提交級別一樣,但是在讀取的時候會保持共享鎖,一直到事務結束。也就是說只要一個事務沒有結束,鎖就不會釋放。其他的事務無法更新數據,保證了不會出現不可重復讀的情況。
最后是可串行讀,它是在可重復讀的基礎上進一步加強了隔離性。在事務進行當中,不僅會鎖定受影響的數據本身,而且還會鎖定整個范圍。這就阻止了其他事務影響整體的情況出現。在這個隔離級別下,保證了事務之間不會有任何踩踏。
到這里,數據庫事務四大原則當中的三個就介紹完了,內容看起來不少,但其實還沒有結束,關于隔離的實現會牽扯到鎖的使用,這塊深挖下去,又會牽扯許多內容。不過對于我們算法從業者而言,能夠了解到這一層,也差不多夠了。
四原則當中還剩下一個一致性原則,一致性這個單詞在很多地方都出現過,比如分布式存儲系統、多副本的一致性等等。但是這些概念的意思并不相同,不可以簡單地理解成同一回事。數據庫的一致性表示數據的狀態是正確的,在轉移的時候,是從一個正確的狀態轉移到了另一個正確的狀態。正確的狀態其實就是指不出錯的狀態,也就是和程序員預期一致的狀態。之前在介紹隔離性時談到的種種問題,總結起來都是數據和程序員的預期不一致。也就是說如果和程序員的預期一致,就可以認為滿足了一致性。
雖然一致性是數據庫的四原則之一,但數據庫系統當中并沒有專門針對一致性的部分。其實在數據庫眼中,滿足了其他三原則,那么自然也就達成了一致性。一致性是目的,并不是手段。舉個例子,還是以剛剛轉賬的情景距離。A向B轉賬100,我們都知道,前提條件是A的賬戶里的金額大于等于100,如果A賬戶里小于100,我們開發的時候沒有做校驗還強行轉賬成功。那么這個結果顯然是錯誤的,也是和我們預期不一致的,但是這個問題發生的原因并不是因為數據庫沒有做好一致性,而是開發人員忽略了限制條件。
所以數據庫的教材上才會寫著“Ensuring the consistency is the responsibility of user, not DBMS.", "DBMS assumes that consistency holds for each transaction”。
“保證一致性是開發的責任,而不是數據庫的,數據庫假設每一個事務都符合 一致性。”
到這里,數據庫事務的四原則就介紹完了,衷心祝大家,日拱一卒,每天都有收獲。
喜歡本文的話,請順手給個關注吧~