幻讀在 InnoDB 中是被如何解決的?

MySQL事務初識中,咱們瞭解到不一樣的事務隔離級別會引起不一樣的問題,如在 RR 級別下會出現幻讀。但若是將存儲引擎選爲 InnoDB ,在 RR 級別下,幻讀的問題就會被解決。在這篇文章中,會先介紹什麼是幻讀、幻讀會帶來引發那些問題以及 InnoDB 解決幻讀的思路。html

實驗環境:RR,MySQL 5.7.27mysql

爲了後面實驗方便,假設在數據庫中有這樣一張表以及數據,注意這裏的 d 列並沒索引:sql

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

什麼是幻讀?

幻讀:是指在同一個事務中,先後兩次查詢相同範圍時,獲得的結果不一致,後一次查詢到新插入的行。數據庫

這裏須要注意的是,因爲在 RR 級別下,普通的讀是快照讀(一致性讀),因此幻讀僅發生在當前讀的基礎上併發

舉例來講:函數

select * from t where d=0 就是快照讀,對於同一個事務來講,每次讀到的結果是同樣的。高併發

select * from t where d=0 in share modeselect * from t where d=0 for update 就是當前讀,老是讀取當前數據行的最新版本,關於數據行版本問題可參考事務究竟有沒有被隔離ui

回到幻讀,有以下 Session:日誌

Session A Session B
begin:
select * from t where d=5 for update;
insert into t values(1,1,5);
select * from t where d=5 for update;
commit;

Session A 第一個 select 結果是:(5,5,5),第二個 select 結果是(1,1,5)和(5,5,5)。因爲兩次當前讀的結果不一致,這就代表出現了幻讀。有一點須要說明,你在嘗試 Session B 會被阻塞,由於在 RR 級別下,默認已經將幻讀的問題的解決,這裏僅做爲思考的過程。code

幻讀帶來的問題?

爲了更好的展示幻讀帶來的問題,爲 Session A,B 添加一條 SQL:

Session A Session B
begin:
select * from t where d=5 for update;
update t set d=100 where d=5;
insert into t values(1,1,5);
update t set d=5 where id=1;
select * from t where d=5 for update;
commit;

1. 破壞了語義*

新的 Session B 中,除了添加一條新記錄外,還修改了新記錄的 d 值。這就破壞了 A 的語義, Session A 的目的就是鎖住全部 d=5 的行,不讓其被操做。

2. 數據一致性的問題

鎖的存在就是爲了不在併發條件下,出現的數據一致性的問題。這裏咱們看下 A,B 提交後數據庫的數據結果:

id=1 插入了一條新的記錄,id=5 的記錄 d 被修改爲 100.

(0,0,0),
(1,5,5);
(5,5,100),
(10,10,10),
(15,15,15),
(20,20,20),
(25,25,25);

上面的結果看似沒有問題,這裏看下生成的 binlog 的執行邏輯,因爲 Session B 先提交,因此對應語句在前:

# Session B 先執行
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/

# Session A 後執行
update t set d=100 where d=5;/*全部d=5的行,d改爲100*/

若是拿此 binlog 進行數據恢復,可見 id=1 的這樣行被修改爲了(1,5,100),這就出現了數據一致性的問題。

如何解決幻讀?

對於 select * from t where d=5 for update; 來講,鎖住d=5對應的行或者鎖住掃描過程當中全部的行都是沒有用的, 由於插入並不影響以前行的操做,因此 InnoDB 爲了解決幻讀,引入了新的鎖 - 間隙鎖。

間隙鎖,會將行之間的空隙鎖住。好比,初始化是插入的 6 個值,就會產生 7 個空隙。

image-20200224165123840

當再執行select * from t where d=5 for update;時,不但會將全表的數據行鎖住,還會將間隙鎖住。

這裏提一下,若是對爲何鎖住全表的數據有疑問,能夠看下以後關於如何加鎖的原則這篇。

事務是否隔離這篇文章中知道,行鎖(Record Lock)按照類型分爲讀鎖和寫鎖,而且行鎖與行鎖在不一樣的事務間是互斥的。

但間歇鎖不一樣,正因爲它解決的是幻讀插入的問題,因此間歇鎖僅僅對插入操做自己互斥,不一樣事務之間的間歇鎖並不互斥。

好比下面這兩個事務:

Session A Session B
begin:
select * from t where c=7 lock in share mode;
update t set d=100 where d=5; begin;
select * from t where c=7 lock in share mode;

因爲 c=7 這條記錄並不存在,出於共同的目的,防止其餘值的插入。Session B 不會被阻塞。Session A 和 Session B 都會爲其加上(5,10)的間歇鎖。

爲了加鎖時的方便,將間歇鎖和行鎖的合集稱爲 next-key lock.行鎖鎖住的是存在的記錄行,間歇鎖鎖住的是行之間的空隙。而 next-key lock 鎖住的是二者之和,好比 select * from t for update 鎖住的就是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

(-∞,0],由間歇鎖 (-∞,0]) 和行鎖 0 組成,其餘相似。

+supremum 表示 InnoDB 給每一個索引加了一個不存在的最大值。

next -key lock 影響併發怎麼辦?

間歇鎖的引入,雖然解決了幻讀的問題,但同時也下降了併發度。

好比下面的業務邏輯,鎖住一行,若是該行不存在就插入不然就更新:

begin;
select * from t where id=N for update;

/*若是行不存在*/
insert into t values(N,N,N);
/*若是行存在*/
update t set d=N set id=N;

commit;

當查詢一條不存在的記錄時,會給所在 id 的間隙加上間隙鎖。假如同時出現併發的狀況,因爲間歇鎖之間不衝突,兩個事務都會加上間歇鎖。以後執行插入時,每一個事務的插入操做與另外事務的間歇鎖出現衝突,進而引起死鎖。

由此看見,間歇鎖的引入致使一樣的語句鎖住更大的範圍,下降了併發度。

假如業務需求並不須要間歇鎖怎麼辦,這時能夠將隔離級別 RC,在此級別下就不存在間歇鎖了。由此引出一個問題,爲何通常在 RC 下,binlog 的格式要設置成 row 呢?

爲何 在 RC 級別下,binlog 格式要設置成 row?

先來看下 binlog 的三種格式:

  • --binlog-format=STATEMENT :在 Master 向 Slave 同步時,會以原生的 SQL 語句進行同步。
  • --binlog-format=ROW :Master 會把被操做後的表中的行記錄在日誌中, 向 Slave 同步。簡單來講同步的就是表中的數據。
  • --binlog-format=MIXED :默認會以 STATEMENT 的方式記錄,但在一些狀況下能夠自動的切換成 ROW 方式,好比執行用戶自定義的函數 UUID.

這裏採用反證法,若是在 RC 級別下,將 binlog 的格式設置成 Statement 會發生什麼?

仍是使用以前 RR 級別下幻讀的例子:

Session A Session B
begin:
update t set d=100 where d=5;
insert into t values(1,1,5);
update t set d=5 where id=1;
commit;

獲得的結果是同樣的,Binlog 日誌中 Session B 先執行,Session A 後執行,A 會把 id=1 中 d 的值改成 100,出現了 binlog 和 數據庫數據不一致的現象。

而基於 ROW 格式則不一樣,binlog 日誌中記錄的是被操做後的數據,不是從新執行 SQL 天然就沒有這個問題。

總結

在這篇文章中,主要介紹了幻讀的問題,知道了 InnoDB 爲了在 RR 級別上解決該問題,引入了間歇鎖。並知道了間歇鎖會下降併發率,增長死鎖狀況的發生。還了解到 next-key lock 其實就是行鎖(Record Lock)和間隙鎖的合集。

在業務不須要 RR 支持下,若是想提升併發率,能夠將隔離級別設置成 RC 並將 binlog 格式設置成 row.

參考

binlog-log-formats

相關文章
相關標籤/搜索