mysql的Read Committed和Repeatable Read隔離級別(讀已提交和可重複讀)

1、共享鎖和排他鎖

1.1、共享鎖

共享鎖也叫S鎖/讀鎖, 作用是鎖住當前事務select的數據行,其實事務可以讀這些數據行,但不能寫。

使用:在查詢語句後面顯式增加 LOCK IN SHARE MODE

SELECT ... LOCK IN SHARE MODE;

1.2、排他鎖

排他鎖也叫X鎖/寫鎖,作用是鎖住事務中使用了排他鎖的數據行,其他事務對這些數據行,既不能讀也不能寫。(誰也莫挨老子!)

使用:
1、MySql 的 InnoDB 引擎會爲insert、update、delete操作中涉及的數據自動加排他鎖(根據where條件語句)
2、對於一般的select語句,InnoDB不會加任何鎖,可加FOR UPDATE,顯式地加排他鎖

SELECT ... FOR UPDATE;

ps.加過排他鎖的數據行在其他事務中不能修改數據,也不能通過for update和lock in share mode的方式查詢數據;但可以直接通過普通select …from…查詢數據(但查到的只是已提交過的數據),因爲普通查詢沒有任何鎖機制。

1.3、總述

綜上,若查詢操作不顯式加鎖,普通select語句無鎖(無鎖的實質是MVCC;Serializable隔離級別的select有鎖),insert、update、delete操作有排他鎖。

2、MVCC

多版本併發控制(Multiversion Concurrency Control)是一種提升併發性的技術。最早的數據庫系統,只有讀讀之間可以併發,讀寫、寫讀、寫寫都要阻塞。引入MVCC後,只有寫寫之間相互阻塞,其他三種操作都可以並行,這樣大幅度提高了InnoDB的併發度。

在內部實現中,InnoDB通過undo log保存每條數據的多個版本,並且能夠找回數據的歷史版本提供給用戶讀,每個事務讀到的數據版本可能是不一樣的。

MVCC只在 Read Committed 和 Repeatable Read 兩個隔離級別下工作。
MVCC 的實現依賴:隱藏字段、Read View、Undo log,我們逐個介紹。

2.1、隱藏字段

InnoDB存儲引擎在每行數據的後面添加了三個隱藏字段:

1、DB_TRX_ID (6字節):表示最近一次對本記錄行作修改(insert | update)的事務ID。InnoDB會把delete操作認作update,不過會更新一個另外的刪除位,將行表示爲deleted。並非真正刪除。

2、DB_ROLL_PTR (7字節):回滾指針,指向當前記錄行的undo log信息

3、DB_ROW_ID (6字節):隨着新行插入而單調遞增的行ID。理解:當表沒有主鍵或唯一非空索引時,innodb就會使用這個行ID自動產生聚簇索引。如果有,聚簇索引就不會包含這個行ID了。DB_ROW_ID跟MVCC關係不大。
在這裏插入圖片描述

2.2、Read View

Read View 讀視圖,跟快照、snapshot是一個概念。主要是用來做可見性判斷的, 裏面保存了對本事務不可見的其他活躍事務。首先我們來認識一下Read View裏面的變量:

low_limit_id:目前出現過的最大的事務ID+1,即下一個將被分配的事務ID。

up_limit_id:活躍事務列表trx_ids中最小的事務ID(如果trx_ids爲空,則up_limit_id 爲 low_limit_id)

trx_ids:Read View創建時其他未提交的活躍事務ID列表。意思就是創建Read View時,將當前未提交事務ID記錄下來,後續即使它們修改了記錄行的值,對於當前事務也是不可見的。
注意:Read View中trx_ids的活躍事務,不包括當前事務自己和已提交的事務(即只包括正在內存中的事務)

creator_trx_id:當前創建事務的ID,是一個遞增的編號

2.3、Undo log

Undo log中存儲的是老版本數據,當一個事務需要讀取記錄行時,如果當前(最新)記錄行不可見,可以順着undo log鏈找到滿足其可見性條件的記錄行版本。

大多數對數據的變更操作包括 insert/update/delete,在InnoDB裏,undo log分爲如下兩類:

①.insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 並且在事務提交後就可以立即丟棄。

②.update undo log : 事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌纔會被purge線程刪除。

Purge線程:爲了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下舊記錄的deleted_bit,並不真正將舊記錄刪除。
爲了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit爲true的記錄。purge線程自己也維護了一個read view,如果某個記錄的deleted_bit爲true,並且DB_TRX_ID相對於purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。

2.4、update的具體流程

現假設有一條記錄行如下
在這裏插入圖片描述
1.事務A(事務號爲2)update該記錄的Honor列,將其改爲"FMVP":

①. 事務A先對該行加X鎖
②. 把該行數據拷貝到undo log中,作爲舊版本
③. 修改該行的Honor爲"FMVP",並且修改DB_TRX_ID爲2(事務A的號), 回滾指針指向undo log中的舊版本。(然後還會將修改後的最新數據寫入redo log)
④. 事務提交,釋放排他鎖
在這裏插入圖片描述

2.事務B(事務號爲3)修改同一個記錄行,將Name修改爲"Iguodala":

①. 事務B先對該行加排它鎖
②. 把該行數據拷貝到undo log中,作爲舊版本
③. 修改該行Name爲"Iguodala",並且修改DB_TRX_ID爲3(事務B的號), 回滾指針指向undo log中最新的舊版本。
④. 事務提交,釋放排他鎖
在這裏插入圖片描述

不同事務或者相同事務的對同一記錄行的修改,會使該記錄行的undo log成爲一條鏈表,undo log的鏈首是最新的舊記錄,鏈尾是最早的舊記錄。

2.5、可見性比較算法

在innodb中,創建一個新事務,執行第一個select語句時,innodb會創建一個快照(read view),快照中保存了系統當前不應該被本事務看到的其他活躍事務的id列表(即trx_ids)。當用戶在這個事務中要讀取某個記錄行的時候,innodb會將該記錄行的DB_TRX_ID與該Read View中的一些變量進行比較,判斷是否滿足可見性條件。

假設當前事務要讀取某一個記錄行,該記錄行的DB_TRX_ID(即最新修改該行的事務ID)爲trx_id;Read View中的活躍事務列表trx_ids中最早的事務ID爲up_limit_id,生成這個Read Vew時系統出現過的最大的事務ID+1記爲low_limit_id(即還未分配的事務ID)。
(速記:up是早,low是遲;trx_id爲最新修改)

1、如果 trx_id < up_limit_id, 那麼表明“最新修改該行的事務”在“當前事務”創建快照之前就提交了,所以該記錄行(trx_id)的值對當前事務是可見的。跳到步驟5。
(trx_id修改早早就提交過了,所以返回trx_id的值即可)

2、如果 trx_id >= low_limit_id, 那麼表明“最新修改該行的事務”在“當前事務”創建快照之後才修改該行,所以該記錄行(trx_id)的值對當前事務不可見。跳到步驟4。
(trx_id遲遲纔開始修改,當前事務肯定不可見,於是向前找可見的數據)

3、如果 up_limit_id <= trx_id < low_limit_id, 表明“最新修改該行的事務”在“當前事務”創建快照時可能處於“活動狀態”或者“已提交狀態”;所以就要對活躍事務列表trx_ids進行查找(源碼中是用的二分查找,因爲本身是有序的):

(1) 如果在活躍事務列表trx_ids中能找到 id 爲 trx_id 的事務,表明在“當前事務”創建快照前(即select前),“該記錄行的值”被“id爲trx_id的事務”修改了,但沒有提交;或者在“當前事務”創建快照後,“該記錄行的值”被“id爲trx_id的事務”修改了(不管有無提交);這些情況下,這個記錄行的值對當前事務都是不可見的,跳到步驟4;

(2)在活躍事務列表中找不到,則表明“id爲trx_id的事務”在修改“該記錄行的值”後,在“當前事務”創建快照前就已經提交了,所以記錄行對當前事務可見,跳到步驟5。

4、在該記錄行的 DB_ROLL_PTR 指針所指向的undo log回滾段中,取出最新的的舊事務號DB_TRX_ID, 將它賦給trx_id,然後跳到步驟1重新開始判斷。

5、將該可見行的值返回。

在這裏插入圖片描述
源碼:

//函數:read_view_sees_trx_id。
//read_view中保存了當前全局的事務的範圍:【low_limit_id, up_limit_id】

//1. 當行記錄的事務ID小於Read View的最小活躍事務id,可見
  if (trx_id < view->up_limit_id) {
    return(TRUE);
  }
//2. 當行記錄的事務ID大於等於Read View的最大活躍事務id,不可見
  if (trx_id >= view->low_limit_id) {
    return(FALSE);
  }
//3. 當行記錄的事務ID二者之間,判斷trx_id是否在活躍事務列表中,如果在就不可見,不在就可見
  for (i = 0; i < n_ids; i++) {
    trx_id_t view_trx_id
      = read_view_get_nth_trx_id(view, n_ids - i - 1);
    if (trx_id <= view_trx_id) {
    return(trx_id != view_trx_id);
    }
  }

RR和RC的Read View產生的區別

①. 在innodb中的Repeatable Read級別, 只有事務在begin之後,執行第一條select時, 纔會創建一個快照(read view),將當前系統中活躍的其他事務記錄起來;並且事務之後都是使用的這個快照,不會重新創建,直到事務結束。
②. 在innodb中的Read Committed級別, 事務在begin之後,執行每條select語句時,快照會被重置,即會重新創建一個快照(read view)。

在同一個事務中,select只能看到快照創建前已經提交的修改和該事務本身做的修改。

3、Read Committed 讀已提交

此隔離級別要求當前事務不能讀到其他事務的未提交數據,我們來親測一下。
1)新建兩個連接,來模擬兩個事務(注意是新建"連接"而不是新建"查詢編輯器")。下面成爲連接A和連接B
2)新建表
在這裏插入圖片描述
3)由於mysql默認的事務隔離級別是Repeatable Read,所以先分別在兩個連接中,把隔離級別改爲Read Committed

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

查詢當前隔離級別可使用:

SELECT @@transaction_isolation;

4)在連接A中開啓事務,查詢id=1的記錄

START TRANSACTION;
SELECT id,class_name,teacher_id FROM class_teacher WHERE id=1;

結果:
在這裏插入圖片描述
5)在連接B中開啓事務,更新id=1記錄,且先不提交

START TRANSACTION;
UPDATE class_teacher SET class_name='初三三班' WHERE teacher_id=30; 

6)在當前連接中再次查詢id=1的記錄

SELECT id,class_name,teacher_id FROM class_teacher WHERE id=1;

結果:
在這裏插入圖片描述
可以看到,兩次查詢結果一致。雖然兩次查詢之間,有另一個事務對數據進行了修改,但由於此修改事務沒有提交,所以讀的仍然是“舊數據”。
7)把連接B中的事務提交

COMMIT;

8)再次在連接A中查詢

SELECT id,class_name,teacher_id FROM class_teacher WHERE id=1;

在這裏插入圖片描述
可以看到,這時查詢到的便是修改後的數據了。
這就是“讀已提交”,一句話:select到的永遠是已提交過的數據,未提交的數據不算數!

4、Repeatable Read 可重複讀

事務A前後進行了兩次select,其間事務B插入並提交了一條(符合select條件)的數據,但事務A兩次select的結果仍然保持一致,叫可重複讀。
我們常常說的“讀”,其實指的是“快照讀”(也即上文說的普通讀),具體來說就是:

select ... from ... where ...

快照讀不加鎖,依賴MVCC進行事務隔離,可保證讀已提交、可重複讀和部分幻讀,讀到的可能是歷史數據。

還有另外一種讀,叫“當前讀”,返回最新數據。具體指的是

//加S鎖
select ... from ... where ... lock in share mode
//加X鎖
select ... from ... where ... for update
//加X鎖
update ... set ... where ...
delete from ... where...
insert into ... 

很多人到這裏會暈,這裏提供一種快速釐清的方法:我們現在在研究的,是事務A中的讀!(具體來說就是寫在事務A中的上述6種語句)

4.1、當前讀

前面說了,普通讀即快照讀依賴MVCC;那當前讀依賴什麼來進行事務隔離呢?答案是Next-Key鎖。
Next-Key鎖 (行級鎖) = S鎖/X鎖 (record lock) + 間隙鎖(gap lock)

事實上,“當前讀”除了會上S/X鎖,還會上一把間隙鎖。共享鎖和排他鎖保證了“當前讀”的讀已提交和可重複讀;間隙鎖解決“當前讀”的幻讀問題。(複習一下:所謂幻讀就是兩次(當前)讀之間被插入了一條數據)

4.1.1、間隙鎖

我們通過例子來理解gap鎖。
表class_teacher中有兩條數據
在這裏插入圖片描述
1、把隔離級別改回Repeatable Read並開啓事務A並進行update操作

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION; 
UPDATE class_teacher SET class_name='初三三班' WHERE id<4; 

(這裏的update操作就是讀,當前讀!讀到的就是id=3的記錄)
這時候會鎖住讀到的記錄的前後間隙,此例中會鎖住(0,3)和(3,11)的id,亦即id在此範圍的記錄不能插入!
2、把隔離級別改回Repeatable Read並開啓事務B並進行insert操作

START TRANSACTION; 
INSERT INTO class_teacher VALUES (1,'初三8班',99);

此時insert操作會阻塞!(同理,插入id爲(3,11)的數據也會阻塞;但插入id爲12的數據會成功)
阻塞是由於間隙鎖的存在,當事務A提交,間隙鎖解鎖後,纔可以正常插入之前被事務A間隙鎖封住的行!

總結:由於間隙鎖的存在,所以事務A中,多次根據where條件(當前)讀出的結果,保持一致。這就防止了當前讀的幻讀的出現。
(比如上面的例子,如果事務B能成功插入id=1的記錄的話,那事務A的第一次中update操作(根據where id<4)能讀出1條記錄,第二次update操作卻能讀出2條記錄,從而發生幻讀)

在這裏插入圖片描述

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