數據庫事務原子性、一致性是怎樣實現的?[轉]


這個問題的有趣之處,不在於問題本身(“原子性、一致性的實現機制是什么”),而在於回答者的分歧反映出來的另外一個問題:原子性和一致性之間的關系是什么?

 

我特別關注了@我練功發自真心 的答案,他正確地指出了,為了保證事務操作的原子性,必須實現基於日志的REDO/UNDO機制。但這個答案仍然是不完整的,因為原子性並不能夠完全保證一致性

 

按照我個人的理解,在事務處理的ACID屬性中,一致性是最基本的屬性,其它的三個屬性都為了保證一致性而存在的。

 

首先回顧一下一致性的定義。所謂一致性,指的是數據處於一種有意義的狀態,這種狀態是語義上的而不是語法上的。最常見的例子是轉帳。例如從帳戶A轉一筆錢到帳戶B上,如果帳戶A上的錢減少了,而帳戶B上的錢卻沒有增加,那么我們認為此時數據處於不一致的狀態。

 

在數據庫實現的場景中,一致性可以分為數據庫外部的一致性和數據庫內部的一致性。前者由外部應用的編碼來保證,即某個應用在執行轉帳的數據庫操作時,必須在同一個事務內部調用對帳戶A和帳戶B的操作。如果在這個層次出現錯誤,這不是數據庫本身能夠解決的,也不屬於我們需要討論的范圍。后者由數據庫來保證,即在同一個事務內部的一組操作必須全部執行成功(或者全部失敗)。這就是事務處理的原子性。

 

為了實現原子性,需要通過日志:將所有對數據的更新操作都寫入日志,如果一個事務中的一部分操作已經成功,但以后的操作,由於斷電/系統崩潰/其它的軟硬件錯誤而無法繼續,則通過回溯日志,將已經執行成功的操作撤銷,從而達到“全部操作失敗”的目的。最常見的場景是,數據庫系統崩潰后重啟,此時數據庫處於不一致的狀態,必須先執行一個crash recovery的過程:讀取日志進行REDO(重演將所有已經執行成功但尚未寫入到磁盤的操作,保證持久性),再對所有到崩潰時尚未成功提交的事務進行UNDO(撤銷所有執行了一部分但尚未提交的操作,保證原子性)。crash recovery結束后,數據庫恢復到一致性狀態,可以繼續被使用。

 

日志的管理和重演是數據庫實現中最復雜的部分之一。如果涉及到並行處理和分布式系統(日志的復制和重演是數據庫高可用性的基礎),會比上述場景還要復雜得多。

 

但是,原子性並不能完全保證一致性。在多個事務並行進行的情況下,即使保證了每一個事務的原子性,仍然可能導致數據不一致的結果。例如,事務1需要將100元轉入帳號A:先讀取帳號A的值,然后在這個值上加上100。但是,在這兩個操作之間,另一個事務2修改了帳號A的值,為它增加了100元。那么最后的結果應該是A增加了200元。但事實上, 事務1最終完成后,帳號A只增加了100元,因為事務2的修改結果被事務1覆蓋掉了。

 

為了保證並發情況下的一致性,引入了隔離性,即保證每一個事務能夠看到的數據總是一致的,就好象其它並發事務並不存在一樣。用術語來說,就是多個事務並發執行后的狀態,和它們串行執行后的狀態是等價的。怎樣實現隔離性,已經有很多人回答過了,原則上無非是兩種類型的鎖:

 

一種是悲觀鎖,即當前事務將所有涉及操作的對象加鎖,操作完成后釋放給其它對象使用。為了盡可能提高性能,發明了各種粒度(數據庫級/表級/行級……)/各種性質(共享鎖/排他鎖/共享意向鎖/排他意向鎖/共享排他意向鎖……)的鎖。為了解決死鎖問題,又發明了兩階段鎖協議/死鎖檢測等一系列的技術。

 

一種是樂觀鎖,即不同的事務可以同時看到同一對象(一般是數據行)的不同歷史版本。如果有兩個事務同時修改了同一數據行,那么在較晚的事務提交時進行沖突檢測。實現也有兩種,一種是通過日志UNDO的方式來獲取數據行的歷史版本,一種是簡單地在內存中保存同一數據行的多個歷史版本,通過時間戳來區分。

 

鎖也是數據庫實現中最復雜的部分之一。同樣,如果涉及到分布式系統(分布式鎖和兩階段提交是分布式事務的基礎),會比上述場景還要復雜得多。

 

@我練功發自真心 提到,其他回答者說的其實是操作系統對atomic的理解,即並發控制。我不能完全同意這一點。數據庫有自己的並發控制和鎖問題,雖然在原理上和操作系統中的概念非常類似,但是並不是同一個層次上的東西。數據庫中的鎖,在粒度/類型/實現方式上和操作系統中的鎖都完全不同。操作系統中的鎖,在數據庫實現中稱為latch(一般譯為閂)。其他回答者回答的其實是“在並行事務處理的情況下怎樣保證數據的一致性”。

 

最后回到原來的問題(“原子性、一致性的實現機制是什么”)。我手頭有本Database System Concepts(4ed,有點老了),在第15章的開頭簡明地介紹了ACID的概念及其關系。如果你想從概念上了解其實現,把這本書的相關章節讀完應該能大概明白。如果你想從實踐上了解其實現,可以找innodb這樣的開源引擎的源代碼來讀。不過,即使是一個非常粗糙的開源實現(不考慮太復雜的並行處理,不考慮分布式系統,不考慮針對操作系統和硬件的優化之類),要基本搞明白恐怕也不是一兩年的事。

編輯於 2015-11-16

1639 條評論

分享

收藏感謝收起

 

沈傑

PHP開發工程師

53 人贊同了該回答

先借用前輩的一句話:數據庫事務有不同的隔離級別,不同的隔離級別對鎖的使用是不同的,鎖的應用最終導致不同事務的隔離級別。

隔離性分為四個級別:
1讀未提交:(Read Uncommitted)
2讀已提交(Read Committed) 大多數數據庫默認的隔離級別
3可重復讀(Repeatable-Read) mysql數據庫所默認的級別
4序列化(serializable)

 

四個級別的具體實現和不同的請下面細讀:

首先程序是可以並發執行的,同樣,在MySQL中,一個表可以由兩個或多個進程同時來讀寫數據,這是沒有問題的。

 

比如,此時有兩個進程來讀數據,這也沒什么問題,允許。但是如果一個進程在讀某一行的數據的過程中,另一個在進程又往這一行里面寫數據(改、刪),那結果會是如何?同樣,如果兩個進程都同時對某一行數據進行更改,以誰的更改為准?那結果又會怎樣,不敢想象,是不是數據就被破壞掉了。所以此時是沖突的。

既然會沖突就要想辦法解決,靠誰來解決,這時候就是靠鎖機制來維護了。怎么使用鎖來使他們不沖突?

在事務開始的時候可以給要准備寫操作的這一行數據加一個排它鎖,如果是讀操作,就給該行數據一個讀鎖。這樣之后,在修改該行數據的時候,不讓其他進程對該行數據有任何操作。而讀該行數據的時候,其他進程不能更改,但可以讀。讀或寫完成時,釋放鎖,最后commit提交。這時候讀寫就分離開了,寫和寫也就分離開了。
注意:此時加鎖和釋放鎖的過程由mysql數據庫自身來維護,不需要我們人為干涉。mysql開發者給這個解決沖突的方案起了一個名字叫做:讀未提交:(Read Uncommitted)。這也就是事務的第一個隔離性。

 

但是這個程度的隔離性僅僅是不夠的。看下面的測試結果:


1)A修改事務級別為:未提交讀。並開始事務,對user表做一次查詢


2)B事務更新一條記錄

3)此時B事務還未提交,A在事務內做一次查詢,發現查詢結果已經改變

4)B進行事務回滾

5)A再做一次查詢,查詢結果又變回去了

由試驗得知:在一個進程的事務當中,我更改了其中的一行數據,但是我修改完之后就釋放了鎖,這時候另一個進程讀取了該數據,此時先前的事務是還未提交的,直到我回滾了數據,另一個進程讀的數據就變成了無用的或者是錯誤的數據。我們通常把這種數據叫做臟數據,這種情況讀出來的數據叫做賍讀。

 

怎么辦?依然是靠鎖機制。無非是鎖的位置不同而已,之前是只要操作完該數據就立馬釋放掉鎖,現在是把釋放鎖的位置調整到事務提交之后,此時在事務提交前,其他進程是無法對該行數據進行讀取的,包括任何操作。那么數據庫為此種狀態的數據庫操作規則又給了一個名字叫做:讀已提交(Read Committed),或者也可以叫不可重復讀。這也就是事務的第二個隔離性。

 

在某些情況下,不可重復讀並不是問題,比如我們多次查詢某個數據當然以最后查詢得到的結果為主。但在另一些情況下就有可能發生問題,例如對於同一個數據A和B依次查詢就可能不同,A和B就可能打起來了……

繼續看下面的測試結果:


1)把隔離性調為READ-COMMITTED(讀取提交內容)設置A的事務隔離級別,並進入事務做一次查詢

2)B開始事務,並對記錄進行修改

3)A再對user表進行查詢,發現記錄沒有受到影響

4)B提交事務

5)A再對user表查詢,發現記錄被修改

試驗進行到這里,你會發現,在同一個事務中如果兩次讀取相同的數據時,最后的結果卻不一致。這里我們把這種現象稱為:不可重復讀。因為在第一個事務讀取了數據之后,此時另一個事務把該數據給修改了,這時候事務提交,那么另一個事務在第二次讀取的時候,結果就不一樣,一個修改前的,一個是修改后的。

但是細心的你會發現,既然你說此種隔離性是在事務提交后才釋放鎖,那么在試驗過程中,在該數據未提交前,另一個事務為什么也是仍然可以讀取的呀。是我說錯了嗎?不是的,在這里mysql使用了一個並發版本控制機制,他們把它叫做MVCC,通俗的也就是說:mysql為了提高系統的並發量,在事務未提交前,雖然事務內操作的數據是鎖定狀態,但是另一個事務仍然可以讀取,大多數數據庫默認的就是這個級別的隔離性。但mysql不是。

而且不只是在更新數據時出現這個問題,在插入數據時仍然會造成類似的這樣一種現象:mysql雖然鎖住了正在操作的數據行,但它仍然不會阻止另一個事務往表插入新行新的數據。比如:一個事務讀取或更新了表里的所有行,接者又有另一個事務往該表里插入一個新行,在事務提交后。原來讀取或更改過數據的事務又第二次讀取了相同的數據,這時候這個事務中兩次讀取的結果集行數就不一樣。原來更新了所有行,而現在讀出來發現竟然還有一行沒有更新。這就是所謂的幻讀。

為了防止同事務中兩次讀取數據不一致,(包括不可重讀和幻讀),接下來該如何繼續做呢?!

mysql依然采取的是MVCC並發版本控制來解決這個問題。具體是:如果事務中存在多次讀取同樣的數據,MySQL第一次讀的時候仍然會保持選擇讀最新提交事務的數據,當第一次之后,之后再讀時,mysql會取第一次讀取的數據作為結果。這樣就保證了同一個事務多次讀取數據時數據的一致性。這時候,mysql把這種解決方案叫做:可重復度(Repeatable-Read),也就是上述所寫的第三個隔離性,也是mysql默認的隔離級別。

注意:幻讀和不可重復讀(Read Committed)都是讀取了另一條已經提交的事務(這點就臟讀不同),所不同的是不可重復讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。

說到這里,真的就完事了嗎?到這里其實mysql並未完全解決數據的一致性問題。只是在讀取上做了手腳,解決了傳統意義上的幻讀和不可重復讀。
例子:1 A事務開啟,B事務開啟。
2 B事務往表里面插入了一條數據,但還並未提交。
3 A事務開始查詢了,並沒有發現B事務這次插入的數據。然后此時B事務提交了數據。
4 於是乎,A事務就以為沒有這條數據,就開始添加這條數據,但是卻發現,發生了數據 重復沖突。

 

最后這個時候,該我們的最后一種隔離級別也是最高的隔離級:別序列化(serializable)登場了。
該隔離級別會自動在鎖住你要操作的整個表的數據,如果另一個進程事務想要操作表里的任何數據就需要等待獲得鎖的進程操作完成釋放鎖。可避免臟讀、不可重復讀、幻讀的發生。當然性能會下降很多,會導致很多的進程相互排隊競爭鎖。

后記:以上所說的四種隔離性的鎖機制應用是數據庫自動完成的,不需要人為干預。隔離級別的設置只對當前鏈接有效。對於使用MySQL命令窗口而言,一個窗口就相當於一個鏈接,當前窗口設置的隔離級別只對當前窗口中的事務有效

 https://www.zhihu.com/question/30272728


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM