淺析Mysql的隔離級別及MVCC
首發於簡書:淺析Mysql的隔離級別及MVCC
作者簡書ID:小北覓
一、Mysql的四個隔離級別
預備工作: - 先建立一個test資料庫及account表,
create
database
test
;
use
test
;
create
table
account
(
id
int
not
null
,
balance
float
not
null
,
PRIMARY
KEY
(
id
)
)
向account中插入兩條測試資料
INSERT
INTO
table
(
id
,
balance
)
VALUES
(
1
,
1000
);
INSERT
INTO
table
(
id
,
balance
)
VALUES
(
2
,
1000
);
開啟兩個控制檯視窗,當做兩個使用者(A和B)
1。1 READ UNCOMMITTED(未提交讀)
也即RU,在READ UNCOMMITTED級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。事務可以讀取未提交的資料,這也被稱為髒讀(Dirty Read)。這個級別會導致很多問題,從效能上來說,READ UNCOMMITTED不會比其他的級別好太多,但卻缺乏其他級別的很多好處,除非真的有非常必要的理由,在實際應用中一般很少使用。
A使用者操作如下:
set
session
transaction
isolation
level
read
uncommitted
;
start
transaction
;
select
*
from
account
;
結果如下:
B使用者操作如下:
set
session
transaction
isolation
level
read
uncommitted
;
start
transaction
;
update
account
set
balance
=
balance
+
200
where
id
=
1
;
隨後在A使用者終端中查詢資料,結果如下:
可以看到B使用者並未提交事務,但是A使用者卻能讀到未提交的資料,這就是
髒讀
。
1。2 READ COMMITTED(提交讀)
即RC,大多數資料庫系統的預設隔離級別都是READ COMMTTED(但MySQL不是,Mysql的預設隔離級別是REPEATABLE READ)。READ COMMITTED滿足前面提到的隔離性的簡單定義:一個事務開始時,只能”看見”已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。這個級別有時候叫做不可重複讀(nonrepeatble read),因為兩次執行同樣的查詢,可能會得到不一樣的結果。以例子說明:
我們將使用者B所在的會話當前事務隔離級別設定為read commited。
set
session
transaction
isolation
level
read
committed
;
在A所在的會話中執行
update
account
set
balance
=
balance
-
200
where
id
=
1
;
在B使用者的會話中查詢:
select
*
from
account
;
結果如下:
發現數據沒有變,還是1000,說明可以避免髒讀了。 接著A使用者會話中將事務提交:
commit
;
再次在B中查詢,結果如下:
可以看到,B使用者讀取到了A使用者提交的資料。這麼做有什麼問題麼?那就是我們在會話B同一個事務中,讀取到兩次不同的結果。這就造成了不可重複讀,就是兩次讀取的結果不同。
1。3 REPEATABLE READ(可重複讀)
REPEATABLE READ解決了髒讀的問題。該隔離級別保證了在同一個事務中多次讀取同樣記錄結果是一致的。但是理論上,可重複讀隔離級別還是無法解決另外一個幻讀(Phantom Read)的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行(Phantom Row)。Mysql的RR是由“行排它鎖+MVCC”一起實現的。
我們將使用者B所在的會話當前事務隔離級別設定為repeatable read。
set
session
transaction
isolation
level
repeatable
read
;
start
transaction
;
接著在B中查詢資料:
兩條。 然後我們到A使用者會話中插入一條資料:
insert
into
account
(
id
,
balance
)
value
(
3
,
1000
);
在A中檢視是否新增成功:
成功,有三條資料。 回到使用者B會話中,再次查詢:
發現沒有變還是兩條。這時,使用者B想插入一條id=3,balance=1000的資料:
insert
into
account
(
id
,
balance
)
value
(
3
,
1000
);
會報錯:
說是主鍵重複了,可是B使用者剛剛查詢並沒有id=3的記錄。這就是
幻讀
現象。 我的理解是:不可重複讀指的是update操作,而幻讀指的是insert或delete操作。
1。4 SERIALIZABLE(序列化)
SERIALIZABLE是最高的隔離級別。它透過強制事務序列執行,避免了前面說的幻讀的問題。簡單來說,SERIALIZABLE會在讀取每一行資料都加鎖,所以可能導致大量的超時和鎖爭用問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保資料的一致性而且可以接受沒有併發的情況下,才考慮採用該級別。
二、MVCC
首先介紹一下幾個概念:
讀鎖:
也叫共享鎖、S鎖,若事務T對資料物件A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S 鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
寫鎖:
又稱排他鎖、X鎖。若事務T對資料物件A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。
表鎖:
操作物件是資料表。Mysql大多數鎖策略都支援,是系統開銷最低但併發性最低的一個鎖策略。事務t對整個表加讀鎖,則其他事務可讀不可寫,若加寫鎖,則其他事務增刪改都不行。
行級鎖:
操作物件是資料表中的一行。是MVCC技術用的比較多的。行級鎖對系統開銷較大,但處理高併發較好。
MVCC使得大部分支援行鎖的事務引擎,不再單純的使用行鎖來進行資料庫的併發控制,取而代之的是把資料庫的行鎖與行的多個版本結合起來,只需要很小的開銷,就可以實現非鎖定讀,從而大大提高資料庫系統的併發效能。
2。1 重要欄位
Mysql Innodb中行記錄的儲存格式,除了最基本的行資訊外,還會有一些額外的欄位,這裡主要介紹和MVCC有關的欄位:DATA_TRX_ID和DATA_ROLL_PTR。
DATA_TRX_ID
:用來標識最近一次對本行記錄做修改(insert|update)的事務的識別符號, 即最後一次修改(insert|update)本行記錄的事務id。
DATA_ROLL_PTR
:指寫入回滾段(rollback segment)的 undo log record (撤銷日誌記錄記錄)。如果一行記錄被更新, 則 undo log record 包含 ‘重建該行記錄被更新之前內容’ 所必須的資訊。
借圖舉例:出自<<唐成-2016PG大會-資料庫多版本實現內幕。pdf>>
當插入的是一條新資料時,記錄上對應的回滾段指標為NULL
DB_TRX_ID記錄了行的建立的時間,刪除的時間在每個事件發生的時候,每行儲存版本號,而不是儲存事件實際發生的時間。每次事物的開始這個版本號都會增加。自記錄時間開始,每個事物都會儲存記錄的系統版本號。依照事物的版本來檢查每行的版本號。 - 在insert操作時, “建立時間”=DB_TRX_ID,這時,“刪除時間”是未定義的; - 在update操作時,複製新增行的“建立時間”=DB_TRX_ID,刪除時間未定義,舊資料行“建立時間”不變,刪除時間=該事務DB_TRX_ID; - 在delete操作時,相應資料行的“建立時間”不變,刪除時間=該事務的DB_ROW_ID; - 在select操作時,對兩者都不修改,只讀相應的資料。
2。2 原理
InnoDB的MVCC,是透過在每行紀錄後面儲存兩個隱藏的列來實現的。這兩個列,一個儲存了行的建立時間,一個儲存了行的過期時間(或刪除時間),當然儲存的並不是實際的時間值,而是系統版本號。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢到的每行紀錄的版本號進行比較。在REPEATABLE READ隔離級別下,MVCC具體的操作如下:
SELECT InnoDB會根據以下兩個條件檢查每行紀錄: - InnoDB只查詢版本早於當前事務版本的資料行,即,行的系統版本號小於或等於事務的系統版本號,這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。 - 行的刪除版本,要麼未定義,要麼大於當前事務版本號。這樣可以確保事務讀取到的行,在事務開始之前未被刪除。 只有符合上述兩個條件的紀錄,才能作為查詢結果返回。
INSERT - InnoDB為插入的每一行儲存當前系統版本號作為行版本號。
DELETE - InnoDB為刪除的每一行儲存當前系統版本號作為行刪除標識。
UPDATE - InnoDB為插入一行新紀錄,儲存當前系統版本號作為行版本號,同時,儲存當前系統版本號到原來的行作為行刪除標識。
優點: 儲存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀資料操作很簡單,效能很好。
缺點: 每行紀錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。
讀到這裡,也許會有一個疑問,考慮如下執行序列:
按照之前的Select規則,會話B 的事務是在 會話A的後面開啟的,那麼B的事務版本號大於A的事務版本號。這樣在A中插入的資料在未提交的情況下,B可以讀到A修改的資料,這不就自相矛盾了麼?其實不然,InnoDB每個事務在開始的時候,會將當前系統中的活躍事務列表(trx_sys->trx_list)建立一個副本(read view),然後一致性讀去比較記錄的tx id的時候,並不是根據當前事務的tx id,而是根據read view最早一個事務的tx id(read view->up_limit_id)來做比較的,這樣就能確保在事務B之前沒有提交的所有事務的變更,B事務都是看不到的。如下圖所示:
結束。rm -rf / 跑
參考資料: 《唐成-2016PG大會-資料庫多版本實現內幕。pdf》