當前位置:
首頁 > 最新 > 簡介事務ACID的實現機制

簡介事務ACID的實現機制

一、概述

事務的ACID特性是關係型資料庫的重要特點,在很多領域有著重要作用。本文試圖介紹下事務ACID的實現機制,不求全面和準確,僅提供一些思路,以在未來遇到類似場景時,能有一點啟發。


二、ACID特性

A-原子性:事務中的statement操作,要麼同時成功,要麼同時失敗。事務是執行的不可分割的最小單元;

C-一致性:一致性在計算機領域有很多不同的解釋,比如在分散式理論中一般是指「各節點保證各自的數據副本同步一致」,而在事務這裡的C,指的是在事務執行之後,數據仍然滿足如級聯、約束等這些規則;

I-隔離:當多個事務同時執行的時候,它們在數據可見性上的隔離程度。分別是Read Uncommitted、Read Committed、Repeatable Read、Serializable;

D-持久性:當客戶端一旦收到資料庫返回成功的消息,那麼事務中的數據就被永久保存,不會丟失;

那這幾個特性,關係型資料庫一般是如何做到的呢? 主要依靠MVCC(MultiVersion Concurrent Control)和WAL(Write Ahead Log)。其中MVCC控制的是ACID中的I部分,WAL控制的是AD部分。C特性由誰保證呢? 我理解在保證了AD特性——不會在事務結束後出現半截寫入、或部分丟失的問題——也就滿足了C特性。


如果把事務理解為線程,I特性歸根結底是個多線程的並發模型——在「多事務同時對一條數據進行操作的時候,是否影響到其他事務」,以及「如果影響的話,會是怎麼影響」。

在線程模型中,如果針對同一塊內存區域進行修改,那麼最簡單的辦法,就是當某線程工作的時候,持有該塊內存區域的互斥鎖,完成後釋放;如果要在操作過程中,內存區域對其他線程可讀,那就持有讀寫鎖。有些關係型資料庫也的確是這麼做的,基於Lock-based Concurrent Control來控制隔離級別的。拿RR(Repeatable Read)來說,在第一個select開始,就在數據行上加write lock,一直到該事務結束。在此過程中,別的事務不可以對這些數據行進行寫操作,否則就會導致不可重複讀,這種模型效率比較低。

更高級的線程模型,應該允許最大程度的並發。除了儘可能地縮小同步塊的粒度之外,在機制上,大致有這兩種:

樂觀鎖,以CAS(compare and swap)為代表。在更新前,先compare下,看在自己執行的這段時間內,變數值有沒有變化。如果未變化,則是安全的,繼續操作;如果變化了,則表明其他線程曾經修改過,不可以繼續了。

各線程拷貝副本(Copy-On-Write),這樣各內存可以各自操作各自的,互不影響。但這樣會導致臟數據,即對變數X,A線程更新了(如++),但由於對B線程不能及時可見,如果B線程也更新(如++),則會丟失一次++。因此為了保證必要的可見性,需要定義一些happen-before的局部有序。舉個例子,比如volitale關鍵字要保證,其修飾的變數如果發生了更新,其他線程都要可見。

主流的關係型資料庫,像MySQL/Oracle,基本使用這類機制,稱為MultiVersion Concurrent Control(MVCC)。事務之間讀讀、讀寫、寫讀不僅可以並存(理論上寫寫也可以並存),而且可以保證相互之間正確的可見性。

在介紹MVCC之前,先回顧一個術語read view,在MySQL Innodb手冊中read view的定義是

An internal snapshot used by the MVCC mechanism of InnoDB. Certain transactions, depending on their isolation level, see the data values as they were at the time the transaction (or in some cases, the statement) started. Isolation levels that use a read view are REPEATABLE READ, READ COMMITTED, and READ UNCOMMITTED.

簡單地說,就是事務/Statement執行時所讀到數據的快照,這就很像前麵線程模型中提到的「各線程拷貝副本」,也是Copy-On-Write機制。有些資料庫,是把待修改數據Copy出來之後,在新的副本上執行事務;有些則是Copy出個副本僅作為恢復使用(redo/undo時用),事務在真正的數據段上執行,MySQL的InnoDB是後一種方式。下面以MySQL的InnoDB為例,詳細地說下MVCC機制大概是怎麼設計的。

我們知道InnoDB中,每個數據行會保存額外的三個欄位,分別是:

ROWID:innodb為每個新插入的記錄行,都會生成的唯一ID;

TXID:最近一次在該行執行insert/update的事務ID,該事務可能已commit,也可能未commit;

ROLL_PDR:指向一個數據塊,這數據塊包含了「如果回滾當前事務,所需要恢復的數據」,是undo log中的一個地址;

下面這個case,是「用戶個人信息」表的一條記錄。它的前後變遷歷史是:

事務(TXID=100)於16:00(下午4點鐘)插入了這條記錄,且隨即commit了,此時age=20。

事務(TXID=102)於16:28創建並更新該條記錄,update age=25,且於16:31時commit;

事務(TXID=103)於16:29創建,並第一次select age from table,然後於16:32再一次select age from table;

事務(TXID=104)於16:30創建並更新該條記錄,update age=30,且於16:35時commit;

如下圖(在每次update執行前,都會保存更新前的記錄到undo log,所以這裡有2條undo log

那麼事務(TXID=103),前後2次select age from table,各自應該看到的age是多少呢?不同的隔離級別,看到的內容不一樣。之前我們說過read view不是? 這裡就發揮作用了,在創建read view時,會「保存當時所有活動的(已開始)但未提交的事務ID列表,並記錄下事務ID區間[minTxId,maxTxId)」。下面分別以RR(Repeatable Read)和RC(Read Committed)這兩種隔離級別看一下。事務TXID103在啟動事務時(實際時間點是在事務第一次執行statement(insert/update)時),創建read view。

隔離級別為RR:

在RR級別下,一個事務只會創建一次read view,其執行時間點在事務第一次執行statement(insert/update)時。

在事務TXID103第一次執行select age from table時,發現這是該事務的第一次statement,則創建read view——找出當前正在活動的事務列表。在本例中就是[102,104];

此時它發現當前數據行的TXID=102(TXID103在16:29執行第一次查詢,此時TXID102已經開始,TX104還沒有),把102和[102,104]這個區間對比,發現處於這個區間之內,則數據不可見,因此通過undo log去找其上一個版本,找到TXID100。發現100不在[102,104]之內,所以可見,看到的age=20;

當第二次select age from table時,它查到當前數據行的TXID已經變成104了,同樣把104和[102,104]這個區間對比,發現處於這個區間之內,數據不可見,因此通過undo log去找其上一個版本——本例中就找到TXID102;

發現102也仍然處在[102,104]這個區間內,仍然不可見,所以再往前找,找到TXID100;

此時發現100不處於[102,104]之內了,所以可見,看到的age=20;

可以看到,在RR級別下,前後兩次select age from table,看到的age值是一樣的,雖然在這個過程中,有其他事務(TXID102)修改了age的值且已提交。

隔離級別為RC:

在RC級別下,每個statement(insert/update)執行之前,都會創建一次read view。

在事務TXID103第一次執行select age from table時,創建read view,並找出當前正在活動的事務列表,本例中是[102,104];經過同樣的對比方法,它發現當前數據行的TXID=102處於該區間之內,所以往前找,找到TXID100,因此得到age=20;

當第二次執行select age from table時,再次創建read view,此時的活動事務列表只有[104]了(因為TXID102已經commit結束了),也發現了當前數據行的TXID=104,於是把104和[104]進行比較,發現處於該區間內,所以不可見,只能通過undo log去找其上一個版本——本例中找到TXID102;

發現102已經不在[104]區間內了,所以可見,看到的age=25;

可以看到,在RC級別下,前後兩次select age from table,看到的age值是不一樣的,因為這個過程中,TXID102修改了age值且已提交。

上面這是一個例子,在實際中,InnoDB還不會出現這樣的同時有兩個更新的情況,因為InnoDB的「寫」是獨佔的,即同一時間只允許一個事務對同一個數據行寫入。這在一些事情的處理上會比較簡單些,假如允許多事務寫,雖然通過樂觀鎖機制(類似CAS)可以實現更高的並發,但也會出現一些問題(假設事務A,要更新ROW1和ROW2,更新ROW1成功,而更新ROW2失敗,需要回滾,那麼如果在更新ROW1成功後,事務B也更新了ROW1,那麼此回滾,會導致事務B的更新丟失,俗稱「第一類更新丟失」)。

RR和RC在MVCC中的一個最主要區別,就是創建read view的時機不同——RR是在事務第一條statement語句執行之前創建,而RC是在每條statement執行之前都創建,這個特性是實現它們不同的可見性的核心。下圖是個例子


在以前沒有電池的台式機時代,我們曾經遇到過,如果突然斷電,就有可能會導致數據丟失、或者文件被損壞。資料庫也是以文件的方式存儲,那麼當它在寫入的過程中,如果發生了斷電等瞬時crash的操作,那麼就有可能文件被破壞,或內容丟失,甚至是寫入半截數據(想像一下資料庫正在寫入一條數據,結果在剛插入前3個欄位,斷電了,導致後面一些欄位沒有插入),導致系統處於不一致的狀態。如果這種事情發生,ACID也就無法保證。為了解決此類問題,資料庫系統通過引入Write Ahead Log(WAL)技術來解決這類問題。

Write Ahead Log會在每個事務操作的時候(包括事務begin/commit,以及insert/update),都會先寫日誌文件,再去修改db表數據。當事務commit時,fsync()日誌文件到磁碟之後,保證其被成功持久化之後,才respond客戶端OK。如果在這之後發生crash,則在資料庫重啟時,從WAL日誌(具體來說,是redo log和undo log)中進行恢復。我們回顧下Durability的定義——當客戶端收到服務端返回的OK消息時,意味著消息已經被永久保存。有了WAL之後,是不是就滿足了Durability的要求?

等等,如果在WSL被fsync到磁碟之前,發生crash怎麼辦? 如果按照嚴格ACID的定義,此時是不會respond客戶端OK的,也就是說,客戶端會捕獲到這個現象,並可能重試。在實踐中,比如InnoDB允許適當妥協,比如innodb_flush_log_at_trx_commit參數可以控制在什麼時機flush log到disk。如果是在commit時flush,則保證了嚴格的ACID;如果為了性能,採取1秒刷新一次的策略,就可能會丟失1秒的數據。

那麼WSL要保存哪些內容,以及該如何恢複數據? 下圖很好地闡述了一個例子

由上圖可以看到,WSL的內容主要包括:

LSN:Log Sequence Number,是日誌塊的唯一編號;

type:操作

oldValue:更新前的數據

newValue:更新後的數據

當事務commit的時候,WSL一定要刷新到磁碟,這樣保證了Durability。同時這些更新是個整體操作,這也就同時保證了Atomic。如果發生了crash,那麼在db recover時,分析該WAL日誌,找到之前的數據,並決定是redo還是undo——如果日誌中有commit操作,則redo;如果沒有,則undo。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 張軻1983 的精彩文章:

TAG:張軻1983 |