InnoDB的事務和鎖——事務到底是隔離的還是不隔離的?

參考文章:https://time.geekbang.org/column/article/70562

目錄

引例

1、表結構

2、事務執行流程

3、事務啓動方式

4、“視圖”概念

“快照”在 MVCC 裏是怎麼工作的?

1、行狀態變更及多版本

2、可重複讀

3、一致性視圖

4、數據版本的可見性規則

4、事務A返回的結果爲什麼是k=1?

判斷方式一:利用數據版本的可見性規則

判斷方式二:時間先後順序判斷

更新邏輯

假設事務 C 不是馬上提交的,而是變成了下面的事務 C’,會怎麼樣呢?

事務的可重複讀的能力是怎麼實現的?

在讀提交隔離級別下,結果如何?


引例

1、表結構

一個事務要更新一行,如果剛好有另外一個事務擁有這一行的行鎖,它又不能這麼超然了,會被鎖住,進入等待狀態。問題是,既然進入了等待狀態,那麼等到這個事務自己獲取到行鎖要更新數據的時候,它讀到的值又是什麼呢?

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

2、事務執行流程

圖 1 事務 A、B、C 的執行流程

可重複讀隔離級別下:【事務 B 查到的 k 的值是 3,而事務 A 查到的 k 的值是 1

3、事務啓動方式

begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才真正啓動。

如果你想要馬上啓動一個事務,可以使用 start transaction with consistent snapshot 這個命令。

說明:事務 C 沒有顯式地使用 begin/commit,表示這個 update 語句本身就是一個事務,語句完成的時候會自動提交。

4、“視圖”概念

“快照”在 MVCC 裏是怎麼工作的?

1、行狀態變更及多版本

transaction id:InnoDB 裏面每個事務有一個唯一的事務 ID,叫作 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。

row trx_id:每次事務更新數據的時候,都會生成一個新的數據版本,並且把 transaction id 賦值給這個數據版本的事務 ID,記爲 row trx_id

也就是說,數據表中的一行記錄,其實可能有多個版本 (row),每個版本有自己的 row trx_id。

圖 2 行狀態變更圖

圖 2 中的三個虛線箭頭,就是 undo log;而 V1、V2、V3 並不是物理上真實存在的,而是每次需要的時候根據當前版本和 undo log 計算出來的。比如,需要 V2 的時候,就是通過 V4 依次執行 U3、U2 算出來。

2、可重複讀

可重複讀:一個事務啓動的時候,能夠看到所有已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。如果“上一個版本”也不可見,那就得繼續往前找。還有,如果是這個事務自己更新的數據,它自己還是要認的

3、一致性視圖

在實現上, InnoDB 爲每個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在“活躍”的所有事務 ID。“活躍”指的就是,啓動了但還沒提交。

數組裏面事務 ID 的最小值記爲低水位,當前系統裏面已經創建過的事務 ID 的最大值加 1 記爲高水位

這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。

4、數據版本的可見性規則

基於數據的 row trx_id 和這個一致性視圖的對比結果得到:

圖 3 數據版本可見性規則

對於當前事務的啓動瞬間來說,一個數據版本的 row trx_id,有以下幾種可能:

(1)如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據是可見的;

(2)如果落在紅色部分,表示這個版本是由將來啓動的事務生成的,是肯定不可見的;

(3)如果落在黃色部分,那就包括兩種情況

          a. 若 row trx_id 在數組中,表示這個版本是由還沒提交的事務生成的,不可見;

          b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。

你看,有了這個聲明後,系統裏面隨後發生的更新,是不是就跟這個事務看到的內容無關了呢?因爲之後的更新,生成的版本一定屬於上面的 2 或者 3(a) 的情況,而對它來說,這些新的數據版本是不存在的,所以這個事務的快照,就是“靜態”的了。

InnoDB 利用了“所有數據都有多個版本”的這個特性,實現了“秒級創建快照”的能力。

4、事務A返回的結果爲什麼是k=1?

判斷方式一:利用數據版本的可見性規則

這裏,我們不妨做如下假設:

這樣,事務 A 的視圖數組就是[99,100], 事務 B 的視圖數組是[99,100,101], 事務 C 的視圖數組是[99,100,101,102]

圖 4 事務 A 查詢數據邏輯圖

從圖中可以看到,第一個有效更新是事務 C,把數據從 (1,1) 改成了 (1,2)。這時候,這個數據的最新版本的 row trx_id 是 102,而 90 這個版本已經成爲了歷史版本

第二個有效更新是事務 B,把數據從 (1,2) 改成了 (1,3)。這時候,這個數據的最新版本(即 row trx_id)是 101,而 102 又成爲了歷史版本。

你可能注意到了,在事務 A 查詢的時候,其實事務 B 還沒有提交,但是它生成的 (1,3) 這個版本已經變成當前版本了。但這個版本對事務 A 必須是不可見的,否則就變成髒讀了。

好,現在事務 A 要來讀數據了,它的視圖數組是[99,100]。當然了,讀數據都是從當前版本讀起的。所以,事務 A 查詢語句的讀數據流程是這樣的:

判斷方式二:時間先後順序判斷

一個數據版本,對於一個事務視圖來說,除了自己的更新總是可見以外,有三種情況:

(1)版本未提交,不可見;

(2)版本已提交,但是是在視圖創建後提交的,不可見;

(3)版本已提交,而且是在視圖創建前提交的,可見。

事務 A 的查詢語句的視圖數組是在事務 A 啓動的時候生成的,這時候:

更新邏輯

事務 B 的 update 語句,如果按照一致性讀,好像結果不對哦?

你看圖 5 中,事務 B 的視圖數組是先生成的,之後事務 C 才提交,不是應該看不見 (1,2) 嗎,怎麼能算出 (1,3) 來?

圖 5 事務 B 更新邏輯圖

更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲“當前讀”(current read)

當它要去更新數據的時候,就不能再在歷史版本上更新了,否則事務 C 的更新就丟失了。因此,事務 B 此時的 set k=k+1 是在(1,2)的基礎上進行的操作。

因此,在更新的時候,當前讀拿到的數據是 (1,2),更新後生成了新版本的數據 (1,3),這個新版本的 row trx_id 是 101。

所以,在執行事務 B 查詢語句的時候,一看自己的版本號是 101,最新數據的版本號也是 101,是自己的更新,可以直接使用,所以查詢得到的 k 的值是 3。

除了 update 語句外,select 語句如果加鎖,也是當前讀。

所以,如果把事務 A 的查詢語句 select * from t where id=1 修改一下,加上 lock in share modefor update,也都可以讀到版本號是 101 的數據,返回的 k 的值是 3。下面這兩個 select 語句,就是分別加了讀鎖(S 鎖,共享鎖)和寫鎖(X 鎖,排他鎖)。

假設事務 C 不是馬上提交的,而是變成了下面的事務 C’,會怎麼樣呢?

圖 6 事務 A、B、C'的執行流程

務 C’的不同是,更新後並沒有馬上提交,在它提交前,事務 B 的更新語句先發起了。前面說過了,雖然事務 C’還沒提交,但是 (1,2) 這個版本也已經生成了,並且是當前的最新版本。那麼,事務 B 的更新語句會怎麼處理呢?

這時候,我們在上一篇文章中提到的“兩階段鎖協議”就要上場了。事務 C’沒提交,也就是說 (1,2) 這個版本上的寫鎖還沒釋放。而事務 B 是當前讀,必須要讀最新版本,而且必須加鎖,因此就被鎖住了,必須等到事務 C’釋放這個鎖,才能繼續它的當前讀。

圖 7 事務 B 更新邏輯圖(配合事務 C')

事務的可重複讀的能力是怎麼實現的?

可重複讀的核心就是一致性讀(consistent read);而事務更新數據的時候,只能用當前讀。如果當前的記錄的行鎖被其他事務佔用的話,就需要進入鎖等待。

而讀提交的邏輯和可重複讀的邏輯類似,它們最主要的區別是:

在讀提交隔離級別下,結果如何?

說明一下,“start transaction with consistent snapshot; ”的意思是從這個語句開始,創建一個持續整個事務的一致性快照。所以,在讀提交隔離級別下,這個用法就沒意義了,等效於普通的 start transaction。

圖 8 讀提交隔離級別下的事務狀態圖

事務 A 查詢語句返回的是 k=2,事務 B 查詢結果 k=3

-----------------------------------我是快樂且好學的分割線-----------------------------------

思考:

我用下面的表結構和初始化語句作爲試驗環境,事務隔離級別是可重複讀。現在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是卻發現了一個“詭異”的、改不掉的情況。請你構造出這種情況,並說明其原理。

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);

復現出來以後,請你再思考一下,在實際的業務開發中有沒有可能碰到這種情況?你的應用代碼會不會掉進這個“坑”裏,你又是怎麼解決的呢?

-----------------------------------我是快樂且好學的分割線-----------------------------------

小結:

這篇理論知識很豐富,需要先總結下
1.innodb支持RC和RR隔離級別實現是用的一致性視圖(consistent read view)

2.事務在啓動時會拍一個快照,這個快照是基於整個庫的.
基於整個庫的意思就是說一個事務內,整個庫的修改對於該事務都是不可見的(對於快照讀的情況)
如果在事務內select t表,另外的事務執行了DDL t表,根據發生時間,要嘛鎖住要嘛報錯(參考第六章)

3.事務是如何實現的MVCC呢?
(1)每個事務都有一個事務ID,叫做transaction id(嚴格遞增)
(2)事務在啓動時,找到已提交的最大事務ID記爲up_limit_id。
(3)事務在更新一條語句時,比如id=1改爲了id=2.會把id=1和該行之前的row trx_id寫到undo log裏,
並且在數據頁上把id的值改爲2,並且把修改這條語句的transaction id記在該行行頭
(4)再定一個規矩,一個事務要查看一條數據時,必須先用該事務的up_limit_id與該行的transaction id做比對,
如果up_limit_id>=transaction id,那麼可以看.如果up_limit_id<transaction id,則只能去undo log裏去取。去undo log查找數據的時候,也需要做比對,必須up_limit_id>transaction id,才返回數據

4.什麼是當前讀,由於當前讀都是先讀後寫,只能讀當前的值,所以爲當前讀.會更新事務內的up_limit_id爲該事務的transaction id

5.爲什麼rr能實現可重複讀而rc不能,分兩種情況
(1)快照讀的情況下,rr不能更新事務內的up_limit_id,
    而rc每次會把up_limit_id更新爲快照讀之前最新已提交事務的transaction id,則rc不能可重複讀
(2)當前讀的情況下,rr是利用record lock+gap lock來實現的,而rc沒有gap,所以rc不能可重複讀

發佈了108 篇原創文章 · 獲贊 18 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章