若是數據庫中的事務都是串行執行的,這種方式能夠保障事務的執行不會出現異常和錯誤,但帶來的問題是串行執行會帶來性能瓶頸;而事務併發執行,若是不加以控制則會引起諸多問題,包括死鎖、更新丟失等等。這就須要咱們在性能和安全之間作出合理的權衡,使用適當的併發控制機制保障併發事務的執行。mysql
首先咱們先來了解一下併發事務會帶來哪些問題。併發事務訪問相同記錄大體可概括爲如下3種狀況:web
由於讀取記錄並不會對記錄形成任何影響,因此同個事務併發讀取同一記錄也就不存在任何安全問題,因此容許這種操做。算法
若是容許併發事務都讀取同一記錄,並相繼基於舊值對這一記錄作出修改,那麼就會出現前一個事務所作的修改被後面事務的修改覆蓋,即出現提交覆蓋的問題。sql
另一種狀況,併發事務相繼對同一記錄作出修改,其中一個事務提交以後以後另外一個事務發生回滾,這樣就會出現已提交的修改由於回滾而丟失的問題,即回滾覆蓋問題。數據庫
這兩種問題都形成丟失更新,其中回滾覆蓋稱爲第一類丟失更新問題,提交覆蓋稱爲第二類丟失更新問題。segmentfault
這種狀況較爲複雜,也最容易出現問題。安全
若是一個事務讀取了另外一個事務還沒有提交的修改記錄,那麼就出現了髒讀的問題;ruby
若是咱們加以控制使得一個事務只能讀取其餘已提交事務的修改的數據,那麼這個事務在另外一事物提交修改先後讀取到的數據是不同的,這就意味着發生了不可重複讀;數據結構
若是一個事務根據一些條件查詢到一些記錄,以後另外一事物向表中插入了一些記錄,原先的事務以相同條件再次查詢時發現獲得的結果跟第一次查詢獲得的結果不一致,這就意味着發生了幻讀。併發
對於以上提到的併發事務執行過程當中可能出現的問題,其嚴重性也是不同的,咱們能夠按照問題的嚴重程度排個序:
丟失更新 > 髒讀 > 不可重複讀 > 幻讀
複製代碼
所以若是咱們能夠容忍一些嚴重程度較輕的問題,咱們就能獲取一些性能上的提高。因而便有了事務的四種隔離級別:
Read Uncommitted
):容許讀取未提交的記錄,會發生髒讀、不可重複讀、幻讀;Read Committed
):只容許讀物已提交的記錄,不會發生髒讀,但會出現重複讀、幻讀;Repeatable Read
):不會發生髒讀和不可重複讀的問題,但會發生幻讀問題;但MySQL
在此隔離級別下利用MVCC或者間隙鎖能夠禁止幻讀問題的發生;Serializable
):即事務串行執行,以上各類問題天然也就都不會發生。值得注意的是以上四種隔離級別都不會出現回滾覆蓋的問題,可是提交覆蓋的問題對於MySQL
來講,在Read Uncommitted
、Read Committed
以及Repeatable Read
這三種隔離級別下都會發生(標準的Repeatable Read
隔離級別不容許出現提交覆蓋的問題),須要額外加鎖來避免此問題。
SQL
規範定義了以上四種隔離級別,可是並無給出如何實現四種隔離級別,所以不一樣數據庫的實現方式和使用方式也並不相同。而SQL
隔離級別的標準是依據基於鎖的實現方式來制定的,由於有必要先了解一下傳統的基於鎖的隔離級別是如何實現的。
既然說到傳統的隔離級別是基於鎖實現的,咱們先來了解一下鎖。
傳統的鎖有兩種:
Shared Locks
):簡稱S鎖
,事務對一條記錄進行讀操做時,須要先獲取該記錄的共享鎖。Exclusive Locks
):簡稱X鎖
,事務對一條記錄進行寫操做時,須要先獲取該記錄的排他鎖。須要注意的是,加了共享鎖的記錄,其餘事務也能夠得到該記錄的共享鎖,可是沒法獲取該記錄的排他鎖,即S鎖
和S鎖
是兼容的,S鎖
和X鎖
是不兼容的;而加了排他鎖的記錄,其餘事務既沒法獲取該記錄的共享鎖也沒法獲取排他鎖,即X鎖
和X鎖
也是不兼容的。
另外,剛剛說到事務對一條記錄進行讀操做時,須要先獲取該記錄的S鎖
,但有時事務在讀取記錄時須要阻止其餘事務訪問該記錄,這時就須要獲取該記錄的X鎖
。以MySQL
爲例,有如下兩種鎖定讀的方式:
S鎖
:SELECT ... LOCK IN SHARE MODE;
複製代碼
若是事務執行了該語句,則會在讀取的記錄上加S鎖
,這樣就容許其餘事務也能獲取到該記錄的S鎖
;而若是其餘事務須要獲取該記錄的X鎖
,那麼就須要等待當前事務提交後釋放掉S鎖
。
X鎖
:SELECT ... FOR UPDATE;
複製代碼
若是事務執行了該語句,則會在讀取的記錄上加X鎖
,這樣其餘事務想要說去該記錄的S鎖
或X鎖
,那麼須要等待當前事務提交後釋放掉X鎖
。
對於鎖的粒度而言,鎖又能夠分爲兩種:
在基於鎖的實現方式下,四種隔離級別的區別就在於加鎖方式的區別:
X鎖
且直到事務提交後才釋放。S鎖
,寫操做加X鎖
且直到事務提交後才釋放;讀操做不會阻塞其餘事務讀或寫,寫操做會阻塞其餘事務寫和讀,所以能夠防止髒讀問題。S鎖
且直到事務提交後才釋放,寫操做加X鎖
且直到事務提交後才釋放;讀操做不會阻塞其餘事務讀但會阻塞其餘事務寫,寫操做會阻塞其餘事務讀和寫,所以能夠防止髒讀、不可重複讀。X鎖
且直到事務提交後才釋放,粒度爲表鎖,也就是嚴格串行。這裏面有一些細節值得注意:
S鎖
爲短鎖,寫操做所加X鎖
爲長鎖。S鎖
和寫操做所加X鎖
均爲長鎖,即事務獲取鎖以後直到事務提交後才能釋放,這種把獲取鎖和釋放鎖分爲兩個不一樣的階段的協議稱爲兩階段鎖協議(2-phase locking
)。兩階段鎖協議規定在加鎖階段,一個事務能夠得到鎖可是不能釋放鎖;而在解鎖階段事務只能夠釋放鎖,並不能得到新的鎖。兩階段鎖協議可以保證事務串行化執行,解決事務併發問題,但也會致使死鎖發生的機率大大提高。不一樣數據庫對於SQL
標準中規定的隔離級別支持是不同的,數據庫引擎實現隔離級別的方式雖然都在儘量地貼近標準的隔離級別規範,但和標準的預期仍是有些不同的地方。
MySQL
(InnoDB
)支持的4種隔離級別,與標準的各級隔離級別容許出現的問題有些出入,好比MySQL
在可重複讀隔離級別下能夠防止幻讀的問題出現,但也會出現提交覆蓋的問題。
相對於傳統隔離級別基於鎖的實現方式,MySQL
是經過MVCC
(多版本併發控制)來實現讀-寫併發控制,又是經過兩階段鎖來實現寫-寫併發控制的。MVCC
是一種無鎖方案,用以解決事務讀-寫併發的問題,可以極大提高讀-寫併發操做的性能。
爲了方便描述,首先咱們建立一個表book
,就三個字段,分別是主鍵book_id
, 名稱book_name
, 庫存stock
。而後向表中插入一些數據:
INSERT INTO book VALUES(1, '數據結構', 100);
INSERT INTO book VALUES(2, 'C++指南', 100);
INSERT INTO book VALUES(3, '精通Java', 100);
複製代碼
對於使用InnoDB
存儲引擎的表,其聚簇索引記錄中包含了兩個重要的隱藏列:
DB_TRX_ID
):每當事務對聚簇索引中的記錄進行修改時,都會把當前事務的事務id記錄到DB_TRX_ID
中。DB_ROLL_PTR
):每當事務對聚簇索引中的記錄進行修改時,都會把該記錄的舊版本記錄到undo
日誌中,經過DB_ROLL_PTR
這個指針能夠用來獲取該記錄舊版本的信息。若是在一個事務中屢次對記錄進行修改,則每次修改都會生成undo
日誌,而且這些undo
日誌經過DB_ROLL_PTR
指針串聯成一個版本鏈,版本鏈的頭結點是該記錄最新的值,尾結點是事務開始時的初始值。
例如,咱們在表book
中作如下修改:
BEGIN;
UPDATE book SET stock = 200 WHERE id = 1;
UPDATE book SET stock = 300 WHERE id = 1;
複製代碼
那麼id=1
的記錄此時的版本鏈就以下圖所示:
對於使用Read Uncommitted
隔離級別的事務來講,只須要讀取版本鏈上最新版本的記錄便可;對於使用Serializable
隔離級別的事務來講,InnoDB
使用加鎖的方式來訪問記錄。而Read Committed
和Repeatable Read
隔離級別來講,都須要讀取已經提交的事務所修改的記錄,也就是說若是版本鏈中某個版本的修改沒有提交,那麼該版本的記錄時不能被讀取的。因此須要肯定在Read Committed
和Repeatable Read
隔離級別下,版本鏈中哪一個版本是能被當前事務讀取的。因而ReadView
的概念被提出以解決這個問題。
ReadView
至關於某個時刻表記錄的一個快照,在這個快照中咱們能獲取到與當前記錄相關的事務中,哪些事務是已提交的穩定事務,哪些是正在活躍的事務,哪些是生成快照以後纔開啓的事務。由此咱們就能根據可見性比較算法判斷出版本鏈中能被讀取的最新版本記錄。
可見性比較算法是基於事務ID的比較算法。首先咱們須要知道的一個事實是:事務id
是遞增分配的。從ReadView
中咱們能獲取到生成快照時刻系統中活躍的事務中最小和最大的事務id
(最大的事務id
其實是系統中將要分配給下一個事務的id
值),這樣咱們就獲得了一個活躍事務id
的範圍,咱們可稱之爲ACTIVE_TRX_ID_RANGE
。那麼小於這個範圍的事務id對應的事務都是已提交的穩定事務,大於這個範圍的事務都是在快照生成以後纔開啓的事務,而在ACTIVE_TRX_ID_RANGE
範圍內的事務中除了正在活躍的事務,也都是已提交的穩定事務。
有了以上信息以後,咱們順着版本鏈從頭結點開始查找最新的可被讀取的版本記錄:
一、首先判斷版本記錄的DB_TRX_ID
字段與生成ReadView
的事務對應的事務ID是否相等。若是相等,那就說明該版本的記錄是在當前事務中生成的,天然也就可以被當前事務讀取;不然進行第2步。
二、若是版本記錄的DB_TRX_ID
字段小於範圍ACTIVE_TRX_ID_RANGE
,代表該版本記錄是已提交事務修改的記錄,即對當前事務可見;不然進行下一步。
三、若是版本記錄的DB_TRX_ID
字段位於範圍ACTIVE_TRX_ID_RANGE
內,若是該事務ID對應的不是活躍事務,代表該版本記錄是已提交事務修改的記錄,即對當前事務可見;若是該事務ID對應的是活躍事務,那麼對當前事務不可見,則讀取版本鏈中下一個版本記錄,重複以上步驟,直到找到對當前事務可見的版本。
若是某個版本記錄通過以上步驟判斷肯定其對當前事務可見,則查詢結果返回此版本記錄;不然讀取下一個版本記錄繼續按照上述步驟進行判斷,直到版本鏈的尾結點。若是遍歷完版本鏈沒有找到對當前事務可見的版本,則查詢結果爲空。
在MySQL
中,Read Committed
和Repeatable Read
隔離級別下的區別就是它們生成ReadView
的時機不一樣。
以前說到ReadView
的機制只在Read Committed
和Repeatable Read
隔離級別下生效,因此只有這兩種隔離級別纔有MVCC
。
在Read Committed
隔離級別下,每次讀取數據時都會生成ReadView
;而在Repeatable Read
隔離級別下只會在事務首次讀取數據時生成ReadView
,以後的讀操做都會沿用此ReadView
。
下面咱們經過例子來看看Read Committed
和Repeatable Read
隔離級別下MVCC
的不一樣表現。咱們繼續以表book
爲例進行演示。
假設在Read Committed
隔離級別下,有以下事務在執行,事務id
爲10:
BEGIN; // 開啓Transaction 10
UPDATE book SET stock = 200 WHERE id = 2;
UPDATE book SET stock = 300 WHERE id = 2;
複製代碼
此時該事務還沒有提交,id
爲2的記錄版本鏈以下圖所示:
而後咱們開啓一個事務對id
爲2的記錄進行查詢:
BEGIN;
SELECT * FROM book WHERE id = 2;
複製代碼
當執行SELECT
語句時會生成一個ReadView
,該ReadView
中的ACTIVE_TRX_ID_RANGE
爲[10, 11)
,當前事務IDcreator_trx_id
爲0
(由於事務中當執行寫操做時纔會分配一個單獨的事務id
,不然事務id
爲0
)。按照咱們以前所述ReadView
的工做原理,咱們查詢到的版本記錄爲
+----------+-----------+-------+
| book_id | book_name | stock |
+----------+-----------+-------+
| 2 | C++指南 | 100 |
+----------+-----------+-------+
複製代碼
而後咱們將事務id
爲10的事務提交:
BEGIN; // 開啓Transaction 10
UPDATE book SET stock = 200 WHERE id = 2;
UPDATE book SET stock = 300 WHERE id = 2;
COMMIT;
複製代碼
同時開啓執行另外一事務id
爲11
的事務,但不提交:
BEGIN; // 開啓Transaction 11
UPDATE book SET stock = 400 WHERE id = 2;
複製代碼
此時id
爲2的記錄版本鏈以下圖所示:
而後咱們回到剛纔的查詢事務中再次查詢id
爲2的記錄:
BEGIN;
SELECT * FROM book WHERE id = 2; // 此時Transaction 10 未提交
SELECT * FROM book WHERE id = 2; // 此時Transaction 10 已提交
複製代碼
當第二次執行SELECT
語句時會再次生成一個ReadView
,該ReadView
中的ACTIVE_TRX_ID_RANGE
爲[11, 12)
,當前事務IDcreator_trx_id
依然爲0
。按照ReadView
的工做原理進行分析,咱們查詢到的版本記錄爲
+----------+-----------+-------+
| book_id | book_name | stock |
+----------+-----------+-------+
| 2 | C++指南 | 300 |
+----------+-----------+-------+
複製代碼
從上述分析能夠發現,由於每次執行查詢語句都會生成新的ReadView
,因此在Read Committed
隔離級別下的事務讀取到的是查詢時刻表中已提交事務修改以後的數據。
咱們在Repeatable Read
隔離級別下重複上面的事務操做:
BEGIN; // 開啓Transaction 20
UPDATE book SET stock = 200 WHERE id = 2;
UPDATE book SET stock = 300 WHERE id = 2;
複製代碼
此時該事務還沒有提交,而後咱們開啓一個事務對id
爲2的記錄進行查詢:
BEGIN;
SELECT * FROM book WHERE id = 2;
複製代碼
當事務第一次執行SELECT
語句時會生成一個ReadView
,該ReadView
中的ACTIVE_TRX_ID_RANGE
爲[10, 11)
,當前事務IDcreator_trx_id
爲0
。根據ReadView
的工做原理,咱們查詢到的版本記錄爲
+----------+-----------+-------+
| book_id | book_name | stock |
+----------+-----------+-------+
| 2 | C++指南 | 100 |
+----------+-----------+-------+
複製代碼
而後咱們將事務id
爲20的事務提交:
BEGIN; // 開啓Transaction 20
UPDATE book SET stock = 200 WHERE id = 2;
UPDATE book SET stock = 300 WHERE id = 2;
COMMIT;
複製代碼
同時開啓執行另外一事務id
爲21的事務,但不提交:
BEGIN; // 開啓Transaction 21
UPDATE book SET stock = 400 WHERE id = 2;
複製代碼
而後咱們回到剛纔的查詢事務中再次查詢id
爲2的記錄:
BEGIN;
SELECT * FROM book WHERE id = 2; // 此時Transaction 10 未提交
SELECT * FROM book WHERE id = 2; // 此時Transaction 10 已提交
複製代碼
當第二次執行SELECT
語句時不會生成新的ReadView
,依然會使用第一次查詢時生成ReadView
。所以咱們查詢到的版本記錄跟第一次查詢到的結果是同樣的:
+----------+-----------+-------+
| book_id | book_name | stock |
+----------+-----------+-------+
| 2 | C++指南 | 100 |
+----------+-----------+-------+
複製代碼
從上述分析能夠發現,由於在Repeatable Read
隔離級別下的事務只會在第一次執行查詢時生成ReadView
,該事務中後續的查詢操做都會沿用這個ReadView
,所以此隔離級別下一個事務中屢次執行一樣的查詢,其結果都是同樣的,這樣就實現了可重複讀。
在Read Committed
和Repeatable Read
隔離級別下,普通的SELECT
查詢都是讀取MVCC
版本鏈中的一個版本,至關於讀取一個快照,所以稱爲快照讀。這種讀取方式不會加鎖,所以讀操做時非阻塞的,所以也叫非阻塞讀。
在標準的Repeatable Read
隔離級別下讀操做會加S鎖
,直到事務結束,所以能夠阻止其餘事務的寫操做;但在MySQL
的Repeatable Read
隔離級別下讀操做沒有加鎖,不會阻止其餘事務對相同記錄的寫操做,所以在後續進行寫操做時就有可能寫入基於版本鏈中的舊數據計算獲得的結果,這就致使了提交覆蓋的問題。想要避免此問題,就須要另外加鎖來實現。
以前提到MySQL
有兩種鎖定讀的方式:
SELECT ... LOCK IN SHARE MODE; // 讀取時對記錄加S鎖,直到事務結束
SELECT ... FOR UPDATE; // 讀取時對記錄加X鎖,直到事務結束
複製代碼
這種讀取方式讀取的是記錄的當前最新版本,稱爲當前讀。另外對於DELETE
、UPDATE
操做,也是須要先讀取記錄,獲取記錄的X鎖
,這個過程也是一個當前讀。因爲須要對記錄進行加鎖,會阻塞其餘事務的寫操做,所以也叫加鎖讀或阻塞讀。
當前讀不只會對當前記錄加行記錄鎖,還會對查詢範圍空間的數據加間隙鎖(GAP LOCK
),所以能夠阻止幻讀問題的出現。
本文介紹了事務的多種併發問題,以及用以免不一樣程度問題的隔離級別,並較爲詳細描述了傳統隔離級別的實現方式以及MySQL
隔離級別的實現方式。但數據庫的併發機制較爲複雜,本文也只是作了大體的描述和介紹,不少細節還須要讀者本身查詢相關資料進行更細緻的瞭解。