讓我們一起了解事務之ACID
本文轉載自微信公眾號「程序員阿sir」,作者程序員阿sir。轉載本文請聯系程序員阿sir公眾號。
隨著科技的飛速發展,人類社會也邁入了大數據時代。很多數據的值不準對我們的生活影響不會很大,比如手表記錄下來的我今天走路步數是10000步,實際就算記成了9500步對我來講也不會太關心。但是有些數據就分毫不能差,比如銀行卡里的錢不能莫名其妙的就少了500塊錢。所有的數據肯定都需要存在在一些數據系統里面,比如數據庫,硬盤,云存儲等等。但是現在的應用越來越復雜,在訪問數據系統經常出現各種問題,比如:
- 寫數據到數據庫的時候出現了軟件或硬件故障導致數據寫了一半失敗了;
- 應用突然崩了;
- 網絡突然斷了導致應用連接不上數據庫了;
- 多個客戶端想要同時更新同一個值導致后者覆蓋了前者。
比如小明想給小華轉賬100塊錢,銀行應用的業務邏輯是如果小明賬號里的余額大于100元時,會從小明的賬號里減去100元,然后把小華的賬號余額加上100元。但是如果剛從小明的賬號里扣除100元時,銀行網斷了導致后面給小華的錢沒加成功,這樣就出現了重大問題,在銀行轉賬時絕對不能發生。所以銀行的應用開發時需要考慮這種異常并進行處理,比如等來網了的時候把小華的賬號里加上100塊錢。
我們可以在應用端處理各種異常,讓應用變得更加魯棒,但是其中這里面有很多細節的問題需要考慮,非常麻煩。而事務 ( Transaction ) 是一種可以簡化問題的機制。
1. 什么是事務
事務可以把一個應用的多個讀寫操作合并為一個邏輯單元。理論來說,一個事務中的所有讀寫操作執行的時候就像是一個操作一樣:或者整個事務的所有操作成功,或者整個事務的所有操作全部失敗。 不允許出現在一個事務中:其中的一部分操作成功了,但是另一部分的操作失敗了的情況。這樣就簡化了問題。比如上面的例子中,把扣除小明賬號的錢和增加小華賬號里的錢看作一個事務,那中間網斷了最多就是沒轉帳成功,兩邊的錢都沒變化,這就保證了不會存在錢轉丟了的情況。這樣也方便應用進行重試 ( Safely Retry )。
大家可能之前都聽說過或用到過事務,看上去好像數據庫就應該使用事務。但是事務并不是本來就天然存在的,他是為了簡化訪問數據庫時的編程模型而被創造出來的概念。使用事務可以幫助開發者忽略一些潛在的數據問題和并發問題,因為實現事務的數據庫本身會幫忙處理這類問題。
那既然事務這么好,是不是所有數據庫都實現了事務,我們就直接用就行,不需要了解事務的具體原理了。實際不是的。因為事務的實現有很多種方式以及不同的級別,他們對應用性能的影響也是不同的。比如一種事務隔離性的實現方式就是加鎖,我們把一個事務里涉及的所有數據行都加上鎖,這樣別的事務根本不能讀寫我們鎖下的這些行數據,直到事務結束才釋放這些鎖,這樣肯定就能避免數據不一致的情況了。但是這樣相當于把讀寫數據串行化了,會非常影響性能。銀行數據非常重要,這樣實現雖然慢但是保證數據的安全了也還能接受。但是假設我們的應用中數據沒有那么重要的情況下,可能我們這種拿性能換數據一致的做法就不太合理了。因此不是每個應用都需要事務。
盡管事務看上去簡單直接,但是實際有很多細節需要考慮情況,我們將會一一進行介紹。首先先介紹一下數據庫中 ACID 的概念。
2. 什么是 ACID
事務提供的安全保證 ( Safety Guarantee )可以用 ACID 來描述分別是:
- 原子性 ( Atomicity )
- 一致性 ( Consistency )
- 隔離性 ( Isolation)
- 持久性 (Durability )。
這是一些事務的安全保證,但是不同數據庫對于ACID的實現可能也是不同的。比如對于隔離性就存在巨大的歧義。所以今天一個數據庫說自己滿足ACID要求,但是可能和你以為的ACID并不一樣。下面分別對這四種安全保證進行解釋。
2.1. 原子性 ( Atomicity )
原子這個詞很容易造成誤解。大家很容易聯想到多線程中的原子操作。如果一個線程執行原子操作,表示其他線程不能看到這個原子操作的中間結果。
但是在 ACID 中,原子性和并發無關,也就是說 ACID 中的原子性不表示當多個進程想要在同一時刻訪問同一數據時會發生什么,但是隔離性表示的是這個意思。
ACID 中的原子性描述了如果一個事務中間出現了錯誤(比如網絡突然斷了)導致這個事務不可能完成,那么事務必須回滾到事務開始之前的狀態。 也就是說刪掉這個事務已經寫到數據庫中的修改。
原子性保證了事務中的操作要么全都發生,要么全都不發生,不可能一半完成了,一半沒做。其實回滾性 (Abortability) 這個名字比原子性更能表示這個特征,但是原子性還是更通用的一個叫法。
舉例來說,小明要給小華轉賬100元。事務里包含兩個操作,第一個是從小明的賬號里扣除100元,第二個是在小華的賬號里增加100元。原子性保證這個事務要么操作全成功,要么操作全失敗,不能出現小明的賬戶里少了100元,小華的賬號里錢數沒變。
2.2. 一致性 (Consistency)
ACID 中的一致性限制了我們自定義的一些數據約束永遠為真。 比如上面銀行轉賬的例子,銀行定義的一個約束條件是無論怎么轉賬,大家的存款總額不變。
但是一致性實際取決于應用本身的定義,也就是說應用負責定義哪些東西需要保持一致性。比如應用邏輯就是要求扣小明100然后給小華賬號加200塊,數據庫也不能阻止他這樣做,因為這個是應用里面定義的。數據庫可能可以加一些外鍵約束或者唯一性約束,但是一般來講,應用負責定義什么數據是合理的,什么是不合理的,數據庫只負責存儲。
有意思的一點是:原子性、隔離性、持久性都是數據庫的性質,而一致性是應用的性質。應用需要依數據庫的原子性和隔離性來實現一致性。所以從某種意義來說,ACID 中的 C 不應該屬于數據庫的安全性保證范疇。
2.3. 隔離性
一個數據庫可以被多個客戶端訪問,如果他們想要讀寫數據庫的不同部分肯定沒有問題,但是如果他們想要同時訪問數據庫的同一條記錄,就有可能產生并發問題 ( Concurrency Problem),也叫競爭條件 (Race Conditions)。
比如下面的例子:
用戶1和用戶2都想增加數據庫中 counter 的值。在修改之前是42。用戶1讀到值為42,在用戶1改變 counter 的值之前,用戶2也讀到了當前值為42,然后用戶1和用戶2分別在原始值上加1,得到43,然后寫入了數據庫。最終數據庫中 counter 的值變成了43,但實際正確的值應該是44。
隔離性要解決的就是這個并發問題。隔離性意思是并發執行的事務之前彼此相互隔離,不應該相互影響。
隔離性是 ACID 中最復雜的,也存在著很多概念上的爭議。大家可能認為隔離性就是可串行化 (Serializability)。意思是數據庫需要保證這些事務在并發情況下運行最后的結果需要和串行運行這些事務的結果一致。實際上這確實是一種隔離性的實現方案,也是最高的隔離級別。但是這種實現卻很少會被用到,因為這會導致性能嚴重受到影響。有些數據庫甚至根本沒有實現這種方案,比如Oracle。Oracle 中有一種隔離級別是“可串行的” (Serializable),但是他實現的是一種較弱的隔離級別--快照隔離 (Snapshot Isolation),有種掛羊頭賣狗肉的感覺... 各種隔離級別的區分我們會在下一篇文章中介紹。
2.4. 持久性
持久性承諾一旦一個事務被成功完成,所有的數據改動將不會回滾。即使硬盤壞了也能保證數據可以被持久存儲。
實際上沒有任何技術可以保證數據絕對持久保存。大家都只是用一些減少數據丟失的技術,比如寫到硬盤、備份等等。
總結
這篇文章介紹了什么是事務以及數據庫安全保證中的 ACID 的含義。下一篇文章將繼續介紹事務的更多細節。
參考文獻
[1] Kleppmann, Martin. Designing data-intensive applications: The big ideas behind reliable, scalable, and maintainable systems. " O'Reilly Media, Inc.", 2017.