您當前的位置:首頁 > 攝影

淺析Mysql的隔離級別及MVCC

作者:由 房東的狗 發表于 攝影時間:2018-09-25

首發於簡書:淺析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

結果如下:

淺析Mysql的隔離級別及MVCC

B使用者操作如下:

set

session

transaction

isolation

level

read

uncommitted

start

transaction

update

account

set

balance

=

balance

+

200

where

id

=

1

隨後在A使用者終端中查詢資料,結果如下:

淺析Mysql的隔離級別及MVCC

可以看到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

結果如下:

淺析Mysql的隔離級別及MVCC

發現數據沒有變,還是1000,說明可以避免髒讀了。 接著A使用者會話中將事務提交:

commit

再次在B中查詢,結果如下:

淺析Mysql的隔離級別及MVCC

可以看到,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中查詢資料:

淺析Mysql的隔離級別及MVCC

兩條。 然後我們到A使用者會話中插入一條資料:

insert

into

account

id

balance

value

3

1000

);

在A中檢視是否新增成功:

淺析Mysql的隔離級別及MVCC

成功,有三條資料。 回到使用者B會話中,再次查詢:

淺析Mysql的隔離級別及MVCC

發現沒有變還是兩條。這時,使用者B想插入一條id=3,balance=1000的資料:

insert

into

account

id

balance

value

3

1000

);

會報錯:

淺析Mysql的隔離級別及MVCC

說是主鍵重複了,可是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>>

淺析Mysql的隔離級別及MVCC

當插入的是一條新資料時,記錄上對應的回滾段指標為NULL

淺析Mysql的隔離級別及MVCC

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為插入一行新紀錄,儲存當前系統版本號作為行版本號,同時,儲存當前系統版本號到原來的行作為行刪除標識。

優點: 儲存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀資料操作很簡單,效能很好。

缺點: 每行紀錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。

讀到這裡,也許會有一個疑問,考慮如下執行序列:

淺析Mysql的隔離級別及MVCC

按照之前的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事務都是看不到的。如下圖所示:

淺析Mysql的隔離級別及MVCC

結束。rm -rf / 跑

參考資料: 《唐成-2016PG大會-資料庫多版本實現內幕。pdf》

標簽: 事務  版本號  id  級別  read