在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 mode
或 select * 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 個空隙。
當再執行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 給每一個索引加了一個不存在的最大值。
間歇鎖的引入,雖然解決了幻讀的問題,但同時也下降了併發度。
好比下面的業務邏輯,鎖住一行,若是該行不存在就插入不然就更新:
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 呢?
先來看下 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.