MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

不正經程式設計師發表於2018-12-28

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

我們之前學習了隔離級別和鎖,在隔離級別裡有一個可重複讀,鎖裡有個行鎖

  • 可重複讀:事務期間,看不懂別的事務的更新;

  • 行鎖:有事務 1 在更新某行資料時,若有其他事務 2 進來,會被鎖住

矛盾來了:事務 2 等待結束,獲取到行鎖時,看到的是哪個資料呢?

按可重複讀隔離級別來說,看到的應該是事務啟動時的最新資料,即事務 1 修改之前的資料;

但是這樣不就造成了事務 1 的修改丟失了嗎?

話不多說,我們先手動實驗一把。

實驗

我們建個表先:

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

然後做如下操作:

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

說明:
1、begin/start transaction 執行後其實並不會立即啟動事務,執行第一個操作 InnoDB 表的語句時才會真正啟動;
2、顯示啟動事務:start transaction with consistent snapshot

很容易看到實驗結果:

  • A 讀到的值是 1

  • B 讀到的值是 3

看上去 B 事務違反了可重複讀隔離級別的概念,為啥呢?

原因探索

之前在學習事務隔離級別時,我們接觸到了一個「檢視」的概念,這個檢視和我們平常接觸的 view 檢視並不一樣。

MySQL 中的兩個「檢視」的區別

一、常說的檢視:view

① 是用查詢語句定義的虛擬表
② 在呼叫時執行查詢,並生成結果;
③ 建立方法:create view...

二、MVCC 中的一致性檢視(consistent read view)

① 用於支援隔離級別的實現:RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)
② 沒有物理結構,作用是事務執行期間用來定義我能看到什麼資料
③ 其中,可重複讀:每個事務啟動是都會重建讀檢視,整個事務存在期間都用這個檢視;

快照是什麼

很多文章都會說可重複讀隔離級別下,事務啟動時會生成整個庫的快照

那麼這個快照是什麼?

我們要先了解下資料的版本問題:

其實每個事務都有一個標識 id:trx_id,是在事務啟動時向儲存引擎的事務系統申請的,並且是按照申請順序嚴格遞增的。

每行資料都有版本的概念,這裡的版本其實就是修改歷史,而這個修改歷史是跟事務掛鉤的,比如:

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

如圖所示,一行資料被多次事務修改時,這行資料會儲存多個版本,如 V1、V2、V3 等。

每個版本會記錄了關聯的事務 id,這裡的版本並不是物理上存在的,需要根據版本號+undo log 來獲取。

其實,快照就是版本號的集合。

事務啟動時發生了什麼

可重複讀隔離級別下,事務的屬性是這樣的:可以看到所有已提交的更新,所有未提交的更新都不能看到。對於同一行資料,以最新一次的事務提交為資料基準。

另外,事務啟動後,很可能存在其他活躍事務(啟動且未提交),我們把這些活躍事務的 id 組成一個陣列,並且記陣列中 trx_id 最小的記為低水位,trx_id 最大的記為高水位

因此,所有的事務 id 可以分成下圖這種:

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

  • trx_id 在綠色部分,已提交,可見

  • 在紅色部分,未提交,不可見

  • 黃色部分

    • trx_id 在陣列中,未提交,不可見

    • trx_id 不在陣列中,已提交,可見

舉個例子

1、假設有一組事務 id:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];

2、其中已提交(可見):[1, 2, 3, 6, 7],未開始的(不可見):[10, 11, 12],當前 id:[8]

3、那麼活躍 id 陣列(不可見):[4, 5, 9],高水位:9,低水位:4;

4、高低水位之間既有已提交但不在陣列中的(可見):[6, 7],又有活躍的(不可見):[4, 5, 9]

實驗覆盤

按照上面的陣列和水位的概念,我們來捋一下文章開頭的實驗。

首先,假設事務 A 的 id 為 10,啟動後,A 的活躍陣列就是 [10];

接下來是事務 B,id 為 11,啟動後,B 的活躍陣列是 [10, 11];

最後是 C,id 為 12,啟動後,活躍陣列是 [10, 11, 12];

C 處理完畢後,直接提交事務;k 的值由 1 變為 2,

此時就儲存了兩個版本的資料:(事務id-9, 1),(事務id-12, 2)

接下來 B 來處理,它會將 k 的值更新為 3,此時就有三個版本的資料:(事務id-9, 1),(事務id-12, 2),(事務id-11, 3)

最後 A 事務,由於 A 啟動時,B、C 事務都未提交,所以它們的資料更新對於 A 來說都是看不到的,因此 A 獲取到的結果是 1。

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

更新的邏輯

不知道你注意到沒有,上面的覆盤中,有一個重要的點,我們沒有說。

按照可重複讀的邏輯,B 執行更新時,看到的 k 的值應該是 1,執行更新的話,就直接造成了 C 操作中的資料丟失。

但是事務 B 在更新時,為什麼讀取到了事務 C 更新的資料?

這裡有一條規則,就是更新資料都是先讀後寫的,而這個讀,只能讀當前的最新值,稱為當前讀(current read),

即,更新資料時總是讀取已經提交完成的最新版本

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

讀鎖(S 鎖,共享鎖):mysql> select k from t where id=1 lock in share mode;
寫鎖(X 鎖,排他鎖):mysql> select k from t where id=1 for update;

總結

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

把握以下幾點:

1、可重複讀的核心就是一致性讀(consistent read);

2、而事務更新資料的時候,只能用當前讀

3、如果當前的記錄的行鎖被其他事務佔用的話,就需要進入鎖等待

另外,本文的幾個重要概念:

1、一致性檢視,保證了當前事務從啟動到提交期間,讀取到的資料是一致的(包括當前事務的修改)。

2、當前讀,保證了當前事務修改資料時,不會丟失其他事務已經提交的修改。

3、兩階段鎖協議,保證了當前事務修改資料時,不會丟失其他事務未提交的修改。

4、RR 是透過事務啟動時建立一致性識圖來實現,RC 是語句執行時建立一致性識圖來實現

課後題目

我們用下面的語句初始化一個表:

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

你是否可以嘗試製造出下面的「詭異」現象?

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

詭異之處在於,我們的目的是將“欄位 c 和 id 值相等的行”的 c 值清零,但是發現更新語句執行成功後,c 的值並沒有被清零!

答案

我們用如下操作流程就可以:

MySQL 實戰 | 08 懵逼,可重複讀好像失效了?

原因分析

主要原因就算當前讀

事務 B 是在事務 A 之後啟動的,但是事務 B 的更新提交是在 事務 A 之前。

事務 A 第一次查詢時,由於可重複讀,讀取到的自然是 1 2 3 4。

事務 A 更新時,根據當前讀規則,此時 c 的值已經是 5,不再滿足更新條件 id=c,因此更新不會真正執行。

所以,事務 A 再次查詢時,獲取到的仍然是 1 2 3 4,事務 A 提交後,再查詢時,獲取到的自然是最新的資料了

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559358/viewspace-2286858/,如需轉載,請註明出處,否則將追究法律責任。

相關文章