Mysql的四個隔離級別 與mvcc(2)

一、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;

結果如下:

001.png

B用戶操作如下:

 

set session transaction isolation level read uncommitted;
start transaction;
update account set balance=balance+200 where id = 1;

隨後在A用戶終端中查詢數據,結果如下:

 

002.png

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

結果如下:

 

003.png

 

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

 

commit;

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

004.png

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

005.png

兩條。
然後我們到A用戶會話中插入一條數據:

 

insert into account(id,balance) value(3,1000);

在A中查看是否添加成功:

 

006.png

 

成功,有三條數據。
回到用戶B會話中,再次查詢:

 

007.png


發現沒有變還是兩條。這時,用戶B想插入一條id=3,balance=1000的數據:

insert into account(id,balance) value(3,1000);

會報錯:

008.png


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

009.png

當插入的是一條新數據時,記錄上對應的回滾段指針爲NULL

 

010.png

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

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

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

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

 

011.png

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

 



作者:小北覓
鏈接:https://www.jianshu.com/p/db334404d909
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章