幻讀:據說有人認爲我是被MVCC幹掉的

幻讀

前言

我是幻讀,據說有人認爲我是MVCC解決的,爲了讓你們更全面地理解我,只能親自來解釋一下。算法

系列文章

1. 揭開MySQL索引神祕面紗 2. MySQL查詢優化必備 3. 上來就問MySQL事務,瑟瑟發抖... 4. MVCC:據說有人好奇個人底層實現 session

1、我是誰?

先給你們作一個簡單的自我介紹,我就是事務併發時會產生的三大問題之一。數據結構

個人其它倆兄弟髒讀、不可重複讀被MVCC在上一個回合無情的幹掉了,至於上個回合發生了什麼能夠去看劇情回顧。併發

個人由來就是由於主人在操做一組數據時還有不少人也在對這組數據進行操做。學習

舉一個簡單的案例:測試

根據條件在對一組數據進行過濾返回的結果爲100個,可是在主人操做的同時其餘人又新增了符合條件的數據,而後主人再次進行查詢時返回結果爲101。第二次返回的數據跟第一次返回數據不一致。優化

因而我誕生了,你們還給我起了個很好聽的名字幻讀url

爲何會給我起這個名字呢!那是由於我給人們的現象好像出了幻覺同樣。.net

2、爲何有人會認爲我是被MVCC幹掉的

爲了演示方便,就直接使用以前的測試表來進行操做。線程

表結構

同時你們能夠看到此表還有一些測試數據,一切從頭開始,清空表。

清空表的命令truncate table_name

執行這個命令會使表的數據清空,而且自增ID會從1開始。

從執行過程來看,truncate table相似於drop table而後在create table,這裏的環境都是測試環境,千萬不要在線上進行操做,由於它繞過了DML方法,是不能回滾的。

清空表

進行了一點小插曲,進入正題。

執行結果

根據上圖的執行步驟,預期來講左邊事務的第一條select語句查詢結果爲空。

第二個select查詢結果爲1條數據,包含右邊事務提交的數據。

但在實際測試的狀況下,第一次執行select和第二次執行select返回結果一致。

從這個案例中,能夠得出結論確實在不可重複隔離級別下會解決幻讀問題(在快照讀的前提下)。

3、我真的是被MVCC解決的?

經過上述測試案例來看,貌似在MySQL中經過MVCC就解決個人引來的問題,那既然都解決了個人問題,爲何還有串行化的隔離級別呢!好疑惑啊!

帶着這個疑問繼續進行實驗,爲了方便就再也不使用上邊表結構了,創建一個簡單的表結構。

表結構

再進入一個小插曲你知道在MySQL終端如何清屏嗎?

執行命令system clear便可

清屏

接着開始新一輪的測試

案例一

上圖案例事務1幾回查詢數據都是空。

此時事務2已經成功將數據插入而且提交。

但當事務1幾回查詢數據爲空以後進行數據插入時,提示主鍵重複。

再來看一個案例

案例二

  • step1:事務1開啓事務
  • step2:事務2開啓事務
  • step3:事務1查詢數據只有一條數據
  • step4:事務2添加一條數據
  • step5:事務1查詢數據爲一條
  • step6:事務2提交事務
  • step7:事務1查詢數據爲一條
  • step8:事務1修改name
  • step9:猜測一下此時表內數據會發生什麼改變

返回結果

此案例中事務1始終讀取數據都是一條數據,可是在修改數據時影響數據行數倒是2,再次進行查看數據時居然出現了事務2添加的數據。這也能夠看做是一種幻讀。

小結

經過以上倆個案例得知在MySQL可重複讀隔離級別中並無徹底解決幻讀問題,而只是解決了快照讀下的幻讀問題

而對於當前讀的操做依然存在幻讀問題,也就是說MVCC對於幻讀的解決是不完全的。

4、再聊當前讀、快照讀

在上一回閤中快照讀、當前讀已經被消化了,爲了防止消化不良這裏再簡單說明一下。

當前讀

全部操做都加了鎖,而且鎖之間除了共享鎖都是互斥的,若是想要增、刪、改、查時都須要等待鎖釋放才能夠,因此讀取的數據都是最新的記錄。

簡單來講,當前讀就是加了鎖的,增、刪、改、查,無論鎖是共享鎖、排它鎖均爲當前讀。

在MySQL的Innodb存儲引擎下,增、刪、改操做都會默認加上鎖,因此增、刪、改操做默認就爲當前讀。

快照讀

快照讀的出現旨在提升事務併發性,實現基於個人敵人MVCC

簡單來講快照讀就是不加鎖的非阻塞讀,即簡單的select操做(select * from user)

在Innodb存儲引擎下執行簡單的select操做時,會記錄下當前的快照讀數據,以後的select會沿用第一次快照讀的數據,即便有其它事務提交也不會影響當前的select結果,這就解決了不可重複讀問題。

快照讀讀取的數據雖然是一致的,但有可能不是最新的數據而是歷史數據。

5、告訴大家吧!當前讀的狀況下我是被next-key locks幹掉的

第二小節中得知在快照讀下因爲我引起的問題已經被MVCC消滅了。

可是在小節三進行案例測試發如今當前讀下我又滿血復活了。

我要是那麼容易被幹掉還怎麼被稱爲打不死的小強,這不是鬧笑話呢!

說歸說,鬧歸鬧若是MVCC把它的小弟next-key locks帶上那我就完了,就再也不像灰太狼說經典語錄「我必定會回來的」

此時就要思考一個問題,在Innodb存儲引擎下,是默認給快照讀加next-key locks,仍是說須要手動加鎖。

經過官方文檔對於next-key locks的解釋。

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the 「gap」 before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.

大體意思,爲了防止幻讀,Innodb使用next-key lock算法,將行鎖(record lock)和間隙鎖(gap lock)結合在一塊兒。Innodb行鎖在搜索或者掃描表索引時,會在遇到的索引記錄上設置共享鎖或者排它鎖,所以行鎖實際是索引記錄鎖。另外, 在索引記錄上設置的鎖一樣會影響索引記錄以前的「間隙(gap)」。即next-key lock是索引記錄行加上索引記錄以前的「gap」上的間隙鎖定。

而且還給了一個案例SELECT * FROM child WHERE id > 100 FOR UPDATE;

當Innodb掃描索引時,會將id大於100地上鎖,阻止任何大於100的數據添加。

到這裏就回答了上邊問題,在Innodb下解決當前讀產生的幻讀問題須要手動加鎖來解決。

再來看一個案例

下圖爲此時的數據狀況

當前數據

下圖的這個案例就解決了在第三節中第一個案例的幻讀問題。

解決幻讀第一個問題

  • step事務1:開啓事務
  • step事務2:開啓事務
  • step事務1:查詢ID爲4的這條數據而且加上排它鎖
  • step事務2:添加ID爲4的數據,而且等待事務1釋放鎖
  • step事務1:添加ID爲4的數據,添加成功
  • step事務1:查詢當前數據
  • step事務1:提交事務
  • step事務2:報錯,返回主鍵重複問題。

這個案例查詢的索引列是主鍵而且是惟一的,此時Innodb引擎會對next-key lock作降級處理,也就是隻鎖定當前查詢的索引記錄行,而不是範圍鎖定。

案例二

仍是使用上邊的數據,可是此次咱們進行一次範圍查找。

回滾數據

此時的數據爲1,3,5,查找的範圍爲大於3。

從下圖能夠看出當事務2執行添加ID爲2的是能夠添加成功的。

可是當添加 ID 6時須要等待。

此時若事務1不提交事務,事務2添加ID爲6的這條數據就執行不成功。

添加區間數據

對於上述的SQL語句select * from user where id > 3 for update;執行返回的只有5這一行數據。

此時鎖定的範圍爲(3,5],(5,∞),因此說id爲2的能夠插入,ID爲4或者大於5的都是插入不了的。

以上就是在Innodb中解決幻讀問題最終方案。

6、幻讀解決方案

爲了方便你們直觀瞭解幻讀的解決方案,這裏咔咔進行簡單的總結。

經過MVCC解決了快照讀下的幻讀問題,爲何能解決?在第一次執行簡單的select語句就生成了一個快照,而且在後邊的select查詢都是沿用第一次快照讀的結果。因此說快照讀查詢到的數據有多是歷史數據。

經過next-key lock解決當前讀的幻讀問題,next-key lock是record lock和gap lock的結合,鎖定的是一個範圍,若是查詢數據爲索引記錄行,則只會鎖定當前行,也就是說降級爲record lock。若爲範圍查找時就會鎖定一個範圍,例如上例中ID爲1,3,5查詢大於3的數據,則會把(3,5],(5,∞)進行範圍鎖定,其它事務在鎖未釋放以前是沒法插入的。

從官方文檔還可得知若是須要驗證數據惟一性只須要給查詢加上共享鎖便可,也就是給select 語句加上 in lock share mode,若是返回結果爲空,則能夠進行插入,而且插入的這個值確定是惟一的。一樣也能夠添加next key lock防止其餘人同時插入相同數據,小節5的全部案例就是使用的next-key lock,從這一點能夠得知next-key lock是能夠鎖定表內不存在的索引。

根據上述結論來看,若是想要檢測數據惟一性使用共享鎖,那麼多個事務同時開啓共享鎖,又同時添加相同的數據怎麼辦,會不會出現問題呢?明確地說明是不會的,若是多個事務同時插入相同數據只會有一個事務添加成功,其它事務會拋出錯誤,這個就是一個新的概念「死鎖」。

7、擴展

事務ID是在什麼時候分配的?

在本文或者其它資料中都能獲得一個信息就是當執行一條簡單的select語句同時也會生成read-view。

雖然快照讀、read-view都是基於事務啓動的前提下,可是read-veiw是經過未提交事務ID組成的。

那麼究竟是在什麼時候分配事務ID的呢?

事務的啓動方式有兩種,分別爲顯示啓動、另外一種是設置autocommit=0後執行select就會啓動事務。

在顯示啓動中最簡單的就是以begin語句開始,也可使用start transaction開啓事務。

若使用start trancaction開啓事務也能夠選擇開始只讀事務仍是讀寫事務。

看了不少資料都說當開啓一個事務時會分配一個事務ID,那麼來驗證一下是這個樣子的嗎?

查詢事務ID

經過上圖能夠看到當執行一個begin語句以後查詢事務ID是空的,也就說當執行begin後並無分配trx_id。

那麼當執行begin後在支持DML語句呢!

查詢事務ID

根據文檔得知

執行begin命令並非真正開啓一個事務,僅僅是爲當前線程設定標記,表示爲顯式開啓的事務。

因此要明白對數據進行了增、刪、改、查等操做後纔算真正開啓了一個事務,此時會去引擎層開啓事務。

爲何事務ID差別特別大?

事務ID

上圖中查詢了當前活躍的事務ID,可是兩個事務ID的差別特別大。

相信不少小夥伴都遇到過這個問題,有問題不懼怕,懼怕的是沒有問題。

事實上在這兩條數據中只有20841是真正的事務ID,那麼第二條數據中的ID是什麼呢!

想知道這個數字是什麼的前提是知道是怎麼來的。

查詢事務id

從上圖能夠看出,當執行select語句後會產生一個很是大的事務ID,那能不能理解爲這種差別很是大的事務ID是經過快照讀的方式纔會生成的。

接着再這個事務下面在執行一個insert語句,而後再查看一下事務ID的狀態

insert以後的事務id

難以想象的是在事務中先執行select語句,而後執行insert語句,事務ID發生了變化,這是什麼緣由呢?

通過資料查詢得知當執行一個簡單的select語句時,被稱之爲只讀事務,爲了不給只讀事務分配trx_id帶來沒必要要的開銷就沒有對其分配事務ID。只讀事務沒有分配undo segment也不會分配LOCK鎖結構,本質上只讀事務的trx_id的值就是0,可是爲了執行select * from information_schema.INNODB_TRX或者show engine innodb status時就會經過reinterpret_cast(trx) | (max_trx_id + 1)將指針轉換爲一個64字節非負整數而後位或(max_trx_id + 1) 就是這麼個值。

關於這個值的生成過程就不用再去深究了,只須要知道在只讀事務下是不會分配事務ID,而查詢出來的這個值只是爲了顯示而存在的沒有實際意義。

可是當你執行select * from information_schema.INNODB_TRX查詢出來的事務ID,再經過show engine innodb status查詢是查不到的。在Innodb下若是事務爲只讀事務則不會在Innodb數據結構中顯示,所以你是看不到的。

堅持學習、堅持寫做、堅持分享是咔咔從業以來一直所秉持的信念。但願在偌大互聯網中咔咔的文章能帶給你一絲絲幫助。我是咔咔,下期見。

相關文章
相關標籤/搜索