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

來源:互聯網
上載者:User

標籤:方式   收藏   是的   日誌   包括   交易處理   repeat   不可重複讀取   更改   

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

 

我特別關注了@我練功發自真心 的答案,他正確地指出了,為了保證事務操作的原子性,必須實現基於日誌的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開發人員給這個解決衝突的方案起了一個名字叫做:讀未提交:(ReadUncommitted)。這也就是事務的第一個隔離性。

 

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


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

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.