淺析資料庫事務的隔離性(isolation)

資料庫事務ACID

資料庫事務可以被定義為一個或者幾個資料庫允許的操作的集合。這個集合需要支持ACID特性。

在ACID特性中,隔離性(isolation)指的是不同事務在提交的時候,最終呈現出來的效果是串列的,換句話說,既是不同事務,按照提交的先後順序執行,再換句話說,對於事務本身來說,它所感知的資料庫,應該只有它自己在操作。那麼最簡單的方法,我們按照定義來實現資料庫事務的隔離性即可,很明顯這在同時並發有多個事務要執行的環境下是沒有執行效率的,一個事務的執行,必然會阻塞其他事務的執行。所以SQL的標準制定者對此作出妥協,提出隔離性的四個等級,其中最高級隔離等級才是串列執行。在沒有到達串列執行等級的情況下,事務又是並發執行的,總是或多或少會存在問題,這在後面的例子當中會講到。

隔離性的四個等級

* 未提交讀(read uncommitted)這個等級是最低等級,也可以認為,事務之間完全不隔離,事務A開始一個事務,接著事務B開始,事務B對數據C繼續update,這時候,A讀取了B未提交(commit)的數據,這種情況叫做臟讀(dirty read)。這個時候要是事務B遇到錯誤必須rollback,那麼A讀取的數據就完全是錯的。可以想像這樣完全不隔離的狀態下,我們相對於資料庫的業務方程序員寫的一個sql,提交個db的執行引擎,返回的結果是多麼不可確定啊。

* 提交讀(read committed)既然讀取別的事務未提交的數據很不安全,那麼在上一個等級完全裸奔的情況下,增加一個要求:事務讀取的數據,都是別的事務已經提交了的。但是只要在還沒達到串列執行的情況下,總會有問題的,事務A select了一條數據,接著事務B update 這條數據,然後commit,這時候A還未提交,A再回來讀這條數據,發現數據居然變了,按照我們之前所說,我們的目標是:對於一個事務本身來說,它所感知的資料庫,應該只有它自己在操作,那麼A會覺得自己並沒有更新數據啊,怎麼數據突然變了,這種情況叫做 不可重複讀(Non-repeatable reads)

* 可重複讀(repeatable read)可重複讀,即是在上一個級別的基礎上,保證不會在一個事務內兩次select同一條數據會出現變化,即是別的事務對你select的對象進行update操作不會影響。但是,如果是insert操作,在這個隔離級別還是會受到影響。事務A開啟事務,並select一段有範圍的數據,然後事務B開啟事務,在先前A事務select的那段有範圍的數據中insert一條數據,然後提交事務,接著事務A再select出來這段數據,發現數據多了一條,這種情況叫幻讀(Phantom Read)

* 序列化讀(serializable)這也就是最高級別,保證事務之間不會有任何踩踏,每個事務都可以認為只有它自己在操作資料庫。

隔離性的實現

我們知道,如果要實現資料庫事務最高隔離性,也就是最安全的隔離性,有個顯而易見的實現就是當一個事務在執行的時候,其他全部事務都阻塞,等待這個事務執行完再執行,這在現代多核CPU環境下顯然非常浪費計算資源。為了充分利用資源,必須支持並發,這裡就涉及並發控制(Concurrency control)

這是一個非常大的主題,關係到資料庫,有兩個比較重要的方法,一個是用鎖(lock),一個是稱為多版本並發控制(MVCC)的方法。

通過鎖的方式來實現隔離性

讀寫鎖

讀寫鎖的概念很平常,當你在讀取數據的時候,應該先加讀鎖,讀取完之後的某個時間再解開讀鎖,那麼加了讀鎖的數據,應該需要有什麼特性呢,應該只能讀,不能寫,因為加了讀鎖,說明有事務準備讀取這個數據,如果被別的事務重寫這個事務,那數據就不準確了。所以一個事務給這個數據加了讀鎖,別的事務也可以對這個數據加讀鎖,因為大家都是只讀不寫。

寫鎖則具有排他性(exclusive lock),當一個事務準備對一個數據進行寫操作的時候,先要對數據加寫鎖,那麼數據就是可變的,這時候,其他事務就無法對這個數據加讀鎖了,除非這個寫鎖釋放。

兩端式提交鎖(Two-phase locking)

兩段式提交分為兩步:

1. 這個階段只加鎖,或者釋放鎖(讀寫鎖)

2. 這個階段只會釋放鎖

下面對應於不同隔離級別對加鎖方式進一步分析:

* 未提交讀(read uncommitted):這個級別加鎖,其實並不需要用兩端式加鎖,每一個具體操作執行完,鎖就可以釋放了。

* 提交讀(read committed):這個階段其實也可以按照每個操作執行前加鎖,執行之後釋放鎖的方式。

* 可重複讀(repeatable read) : 這個級別,就要求讀鎖必須,到事務結束前最後時刻才能釋放,這樣才能保證讀取到數據是不可變的,可重複讀的。但是這樣會阻塞其他事務對加鎖的數據的寫操作。

* 序列化讀(serializable):這個級別要求,兩段式提交的第一步,要在事務開始的時候,原子性的把需要的鎖全部加好(這顯然很難估算,除非很大力度的鎖),在事務結束前最後時刻,把全部鎖一次性釋放。這樣做的結果就是使很多數據在事務執行期能都被加鎖,無法被其他事務所使用。

使用多版本並發控制(MVCC)

加鎖的方式處理事務一個比較大的問題就是會造成死鎖(dead lock),原因就是一個事務加鎖的數據並不止只有一行。事務A對行C加寫鎖,事務B對行D加寫鎖,接著事務希望獲取行D的鎖,事務B希望獲取行C的鎖,這樣很容就死鎖了。

使用MVCC就可以避免很多情況下的加鎖操作,使用數據冗餘的方式來實現事務隔離(這真是個很好的設計啊)

什麼是MVCC

MVCC提供的只是一種思路,具體的實現比較多樣化。大體的思路是每一行保存冗餘數據,讀寫的時間戳,也可以稱為版本號,在對某一行數據繼續update或者delete的時候,並不直接操作,而是複製多一份副本進行操作,這個就是所謂多版本(multiversion)

mysql innodb對於MVCC的實現

innodb對每一行保存兩個系統版本號,一個更新操作的版本號,一個刪除操作的版本號,這兩個版本號的來源是事務的ID(transaction id),也就是說,當某個事務對這一行數據進行update,或者刪除的時候,相應會把它的事務ID寫入這行數據的更新操作的版本號,刪除操作的版本號中。

事務ID是隨時間推移而增長,而且不可重複的。一個事務打開之後:

* 對於select操作:每次只會select具有比當前事務ID更小的更新操作版本號的數據,而且這些數據要保證刪除版本號為空,或者刪除版本號大於當前事務ID。

* 對於update操作:對該行數據複製出一份副本,同時在更新操作版本號寫入當前事務ID,同時把當前事務ID寫入之前的刪除操作的版本號中。

* 對於insert操作:寫入新行,同時在更新操作版本號寫入當前事務ID

* 對於delete操作:在刪除操作版本號寫入當前事務ID

mysql官方innodb的實現是用MVCC,官方聲稱默認的innodb的隔離級別是可重複讀。看了不少資料說無法測試出這種級別下的幻讀,實際上不對的,只是不能用之前加鎖情況下舉例的那個例子。用如下方式可以實現幻讀:事務A開啟,事務B接著開啟,事務B select一段數據,接著事務A 在這段數據中間insert一條數據,接著事務B再select一次,就出現幻讀了。

mysql官方文檔提到串列隔離級別要在原來的基礎說對每一個select操作執行[SELECT ... LOCK IN SHARE MODE](13.2.9 SELECT Syntax)

這樣就可以讀取的數據加讀鎖了,那麼其他試圖寫入數據都必須阻塞。那麼就可以實現序列化串列了。


推薦閱讀:

雲資料庫UDB的三重境界
從國內哪些公司可以買到比較靠譜的 POI 資料庫?
深入淺出hbase和bigtable
簡析關係型資料庫和非關係型資料庫的比較(下)
為什麼Docker容器不適合資料庫服務(一)

TAG:資料庫 | 資料庫設計 | 資料庫性能 |