幻讀:聽說我是被MVCC干掉的?
原創(chuàng)【51CTO.com原創(chuàng)稿件】我是幻讀,聽說有人認(rèn)為我是 MVCC 解決的,為了讓大家更全面地理解我,只能親自來解釋一下。
圖片來自 Pexels
我是誰?
先給大家做一個(gè)簡(jiǎn)單的自我介紹,我就是事務(wù)并發(fā)時(shí)會(huì)產(chǎn)生的三大問題之一。
我的其他倆兄弟臟讀、不可重復(fù)讀被 MVCC 在上一個(gè)回合無情的干掉了,至于上個(gè)回合發(fā)生了什么可以去看劇情回顧。
我的由來就是因?yàn)橹魅嗽诓僮饕唤M數(shù)據(jù)時(shí)還有很多人也在對(duì)這組數(shù)據(jù)進(jìn)行操作。
舉一個(gè)簡(jiǎn)單的案例:根據(jù)條件在對(duì)一組數(shù)據(jù)進(jìn)行過濾返回的結(jié)果為 100 個(gè),但是在主人操作的同時(shí)其他人又新增了符合條件的數(shù)據(jù),然后主人再次進(jìn)行查詢時(shí)返回結(jié)果為 101。第二次返回的數(shù)據(jù)跟第一次返回?cái)?shù)據(jù)不一致。
于是我誕生了,大家還給我起了個(gè)很好聽的名字幻讀。為什么會(huì)給我起這個(gè)名字呢!那是因?yàn)槲医o人們的現(xiàn)象好像出了幻覺一樣。
為什么有人會(huì)認(rèn)為我是被 MVCC 干掉的
為了演示方便,就直接使用之前的測(cè)試表來進(jìn)行操作。
同時(shí)大家可以看到此表還有一些測(cè)試數(shù)據(jù),一切從頭開始,清空表。
清空表的命令:
- truncate table_name
執(zhí)行這個(gè)命令會(huì)使表的數(shù)據(jù)清空,并且自增 ID 會(huì)從 1 開始。
從執(zhí)行過程來看,truncate table 類似于 drop table 然后在 create table,這里的環(huán)境都是測(cè)試環(huán)境,千萬不要在線上進(jìn)行操作,因?yàn)樗@過了 DML 方法,是不能回滾的。
進(jìn)行了一點(diǎn)小插曲,進(jìn)入正題。
根據(jù)上圖的執(zhí)行步驟,預(yù)期來說左邊事務(wù)的第一條 select 語句查詢結(jié)果為空。第二個(gè) select 查詢結(jié)果為 1 條數(shù)據(jù),包含右邊事務(wù)提交的數(shù)據(jù)。
但在實(shí)際測(cè)試的情況下,第一次執(zhí)行 select 和第二次執(zhí)行 select 返回結(jié)果一致。
從這個(gè)案例中,可以得出結(jié)論確實(shí)在不可重復(fù)隔離級(jí)別下會(huì)解決幻讀問題(在快照讀的前提下)。
我真的是被 MVCC 解決的?
通過上述測(cè)試案例來看,貌似在 MySQL 中通過 MVCC 就解決我的引來的問題,那既然都解決了我的問題,為什么還有串行化的隔離級(jí)別呢!好疑惑啊!
帶著這個(gè)疑問繼續(xù)進(jìn)行實(shí)驗(yàn),為了方便就不再使用上邊表結(jié)構(gòu)了,建立一個(gè)簡(jiǎn)單的表結(jié)構(gòu)。
再進(jìn)入一個(gè)小插曲你知道在 MySQL 終端如何清屏嗎?執(zhí)行命令 system clear 即可。
接著開始新一輪的測(cè)試:
上圖案例事務(wù) 1 幾次查詢數(shù)據(jù)都是空。此時(shí)事務(wù) 2 已經(jīng)成功將數(shù)據(jù)插入并且提交。但當(dāng)事務(wù) 1 幾次查詢數(shù)據(jù)為空之后進(jìn)行數(shù)據(jù)插入時(shí),提示主鍵重復(fù)。
再來看一個(gè)案例:
如上圖:
- step1:事務(wù) 1 開啟事務(wù)
- step2:事務(wù) 2 開啟事務(wù)
- step3:事務(wù) 1 查詢數(shù)據(jù)只有一條數(shù)據(jù)
- step4:事務(wù) 2 添加一條數(shù)據(jù)
- step5:事務(wù) 1 查詢數(shù)據(jù)為一條
- step6:事務(wù) 2 提交事務(wù)
- step7:事務(wù) 1 查詢數(shù)據(jù)為一條
- step8:事務(wù) 1 修改 name
- step9:猜想一下此時(shí)表內(nèi)數(shù)據(jù)會(huì)發(fā)生什么改變
此案例中事務(wù) 1 始終讀取數(shù)據(jù)都是一條數(shù)據(jù),但是在修改數(shù)據(jù)時(shí)影響數(shù)據(jù)行數(shù)卻是 2,再次進(jìn)行查看數(shù)據(jù)時(shí)竟然出現(xiàn)了事務(wù) 2 添加的數(shù)據(jù)。這也可以看作是一種幻讀。
小結(jié):通過以上倆個(gè)案例得知在 MySQL 可重復(fù)讀隔離級(jí)別中并沒有完全解決幻讀問題,而只是解決了快照讀下的幻讀問題。
而對(duì)于當(dāng)前讀的操作依然存在幻讀問題,也就是說 MVCC 對(duì)于幻讀的解決是不徹底的。
再聊當(dāng)前讀、快照讀
在上一回合中快照讀、當(dāng)前讀已經(jīng)被消化了,為了防止消化不良這里再簡(jiǎn)單說明一下。
①當(dāng)前讀
所有操作都加了鎖,并且鎖之間除了共享鎖都是互斥的,如果想要增、刪、改、查時(shí)都需要等待鎖釋放才可以,所以讀取的數(shù)據(jù)都是最新的記錄。
簡(jiǎn)單來說,當(dāng)前讀就是加了鎖的,增、刪、改、查,不管鎖是共享鎖、排它鎖均為當(dāng)前讀。
在 MySQL 的 Innodb 存儲(chǔ)引擎下,增、刪、改操作都會(huì)默認(rèn)加上鎖,所以增、刪、改操作默認(rèn)就為當(dāng)前讀。
②快照讀
快照讀的出現(xiàn)旨在提高事務(wù)并發(fā)性,實(shí)現(xiàn)基于我的敵人 MVCC,簡(jiǎn)單來說快照讀就是不加鎖的非阻塞讀,即簡(jiǎn)單的 select 操作(select * from user)。
在 Innodb 存儲(chǔ)引擎下執(zhí)行簡(jiǎn)單的 select 操作時(shí),會(huì)記錄下當(dāng)前的快照讀數(shù)據(jù),之后的 select 會(huì)沿用第一次快照讀的數(shù)據(jù),即使有其它事務(wù)提交也不會(huì)影響當(dāng)前的 select 結(jié)果,這就解決了不可重復(fù)讀問題。
快照讀讀取的數(shù)據(jù)雖然是一致的,但有可能不是最新的數(shù)據(jù)而是歷史數(shù)據(jù)。
告訴你們吧!當(dāng)前讀的情況下,我是被 next-key locks 干掉的
第二小節(jié)中得知在快照讀下由于我引發(fā)的問題已經(jīng)被 MVCC 消滅了。但是在小節(jié)三進(jìn)行案例測(cè)試發(fā)現(xiàn)在當(dāng)前讀下我又滿血復(fù)活了。
我要是那么容易被干掉還怎么被稱為打不死的小強(qiáng),這不是鬧笑話呢!說歸說,鬧歸鬧如果 MVCC 把它的小弟 next-key locks 帶上那我就完了,就不再像灰太狼說經(jīng)典語錄“我一定會(huì)回來的”。
此時(shí)就要思考一個(gè)問題,在 Innodb 存儲(chǔ)引擎下,是默認(rèn)給快照讀加 next-key locks,還是說需要手動(dòng)加鎖。
通過官方文檔對(duì)于 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)結(jié)合在一起。
Innodb 行鎖在搜索或者掃描表索引時(shí),會(huì)在遇到的索引記錄上設(shè)置共享鎖或者排它鎖,因此行鎖實(shí)際是索引記錄鎖。
另外, 在索引記錄上設(shè)置的鎖同樣會(huì)影響索引記錄之前的“間隙(gap)”。即 next-key lock 是索引記錄行加上索引記錄之前的“gap”上的間隙鎖定。
并且還給了一個(gè)案例:
- SELECT * FROM child WHERE id > 100 FOR UPDATE;
當(dāng) Innodb 掃描索引時(shí),會(huì)將 id 大于 100 地上鎖,阻止任何大于 100 的數(shù)據(jù)添加。
到這里就回答了上邊問題,在 Innodb 下解決當(dāng)前讀產(chǎn)生的幻讀問題需要手動(dòng)加鎖來解決。
再來看一個(gè)案例,下圖為此時(shí)的數(shù)據(jù)情況:
下圖的這個(gè)案例就解決了在第三節(jié)中第一個(gè)案例的幻讀問題。
如上圖:
- step事務(wù)1:開啟事務(wù)
- step事務(wù)2:開啟事務(wù)
- step事務(wù)1:查詢 ID 為 4 的這條數(shù)據(jù)并且加上排它鎖
- step事務(wù)2:添加 ID 為 4 的數(shù)據(jù),并且等待事務(wù) 1 釋放鎖
- step事務(wù)1:添加 ID 為 4 的數(shù)據(jù),添加成功
- step事務(wù)1:查詢當(dāng)前數(shù)據(jù)
- step事務(wù)1:提交事務(wù)
- step事務(wù)2:報(bào)錯(cuò),返回主鍵重復(fù)問題
這個(gè)案例查詢的索引列是主鍵并且是唯一的,此時(shí) Innodb 引擎會(huì)對(duì) next-key lock 做降級(jí)處理,也就是只鎖定當(dāng)前查詢的索引記錄行,而不是范圍鎖定。
案例二:還是使用上邊的數(shù)據(jù),但是這次我們進(jìn)行一次范圍查找。
此時(shí)的數(shù)據(jù)為 1,3,5,查找的范圍為大于 3。從下圖可以看出當(dāng)事務(wù) 2 執(zhí)行添加 ID 為 2 的是可以添加成功的。
但是當(dāng)添加 ID 6 時(shí)需要等待。此時(shí)若事務(wù) 1 不提交事務(wù),事務(wù) 2 添加 ID 為 6 的這條數(shù)據(jù)就執(zhí)行不成功。
對(duì)于上述的 SQL 語句 select * from user where id > 3 for update;執(zhí)行返回的只有 5 這一行數(shù)據(jù)。
此時(shí)鎖定的范圍為 (3,5],(5,∞),所以說 id 為 2 的可以插入,ID 為 4 或者大于 5 的都是插入不了的。
以上就是在 Innodb 中解決幻讀問題最終方案。
幻讀解決方案
為了方便大家直觀了解幻讀的解決方案,這里咔咔進(jìn)行簡(jiǎn)單的總結(jié)。
通過 MVCC 解決了快照讀下的幻讀問題,為什么能解決?在第一次執(zhí)行簡(jiǎn)單的 select 語句就生成了一個(gè)快照,并且在后邊的 select 查詢都是沿用第一次快照讀的結(jié)果。所以說快照讀查詢到的數(shù)據(jù)有可能是歷史數(shù)據(jù)。
通過 next-key lock 解決當(dāng)前讀的幻讀問題,next-key lock 是 record lock 和 gap lock 的結(jié)合,鎖定的是一個(gè)范圍,如果查詢數(shù)據(jù)為索引記錄行,則只會(huì)鎖定當(dāng)前行,也就是說降級(jí)為 record lock。
若為范圍查找時(shí)就會(huì)鎖定一個(gè)范圍,例如上例中 ID 為 1,3,5 查詢大于 3 的數(shù)據(jù),則會(huì)把 (3,5],(5,∞) 進(jìn)行范圍鎖定,其它事務(wù)在鎖未釋放之前是無法插入的。
從官方文檔還可得知如果需要驗(yàn)證數(shù)據(jù)唯一性只需要給查詢加上共享鎖即可,也就是給 select 語句加上 in lock share mode,如果返回結(jié)果為空,則可以進(jìn)行插入,并且插入的這個(gè)值肯定是唯一的。
同樣也可以添加 next key lock 防止其他人同時(shí)插入相同數(shù)據(jù),小節(jié) 5 的所有案例就是使用的 next-key lock,從這一點(diǎn)可以得知 next-key lock 是可以鎖定表內(nèi)不存在的索引。
根據(jù)上述結(jié)論來看,如果想要檢測(cè)數(shù)據(jù)唯一性使用共享鎖,那么多個(gè)事務(wù)同時(shí)開啟共享鎖,又同時(shí)添加相同的數(shù)據(jù)怎么辦,會(huì)不會(huì)出現(xiàn)問題呢?
明確地說明是不會(huì)的,如果多個(gè)事務(wù)同時(shí)插入相同數(shù)據(jù)只會(huì)有一個(gè)事務(wù)添加成功,其它事務(wù)會(huì)拋出錯(cuò)誤,這個(gè)就是一個(gè)新的概念“死鎖”。
擴(kuò)展
事務(wù) ID 是在何時(shí)分配的?在本文或者其它資料中都能得到一個(gè)信息就是當(dāng)執(zhí)行一條簡(jiǎn)單的 select 語句同時(shí)也會(huì)生成 read-view。
雖然快照讀、read-view 都是基于事務(wù)啟動(dòng)的前提下,但是 read-veiw 是通過未提交事務(wù) ID 組成的。
①那么到底是在何時(shí)分配事務(wù) ID 的呢?
事務(wù)的啟動(dòng)方式有兩種,分別為顯示啟動(dòng)、另一種是設(shè)置 autocommit=0 后執(zhí)行 select 就會(huì)啟動(dòng)事務(wù)。
在顯示啟動(dòng)中最簡(jiǎn)單的就是以 begin 語句開始,也可以使用 start transaction 開啟事務(wù)。
若使用 start trancaction 開啟事務(wù)也可以選擇開始只讀事務(wù)還是讀寫事務(wù)。
看了很多資料都說當(dāng)開啟一個(gè)事務(wù)時(shí)會(huì)分配一個(gè)事務(wù) ID,那么來驗(yàn)證一下是這個(gè)樣子的嗎?
通過上圖可以看到當(dāng)執(zhí)行一個(gè) begin 語句之后查詢事務(wù) ID 是空的,也就說當(dāng)執(zhí)行 begin 后并沒有分配 trx_id。
那么當(dāng)執(zhí)行 begin 后在支持 DML 語句呢!
根據(jù)文檔得知,執(zhí)行 begin 命令并不是真正開啟一個(gè)事務(wù),僅僅是為當(dāng)前線程設(shè)定標(biāo)記,表示為顯式開啟的事務(wù)。
所以要明白對(duì)數(shù)據(jù)進(jìn)行了增、刪、改、查等操作后才算真正開啟了一個(gè)事務(wù),此時(shí)會(huì)去引擎層開啟事務(wù)。
②為什么事務(wù) ID 差異特別大?
上圖中查詢了當(dāng)前活躍的事務(wù) ID,但是兩個(gè)事務(wù) ID 的差異特別大。相信很多小伙伴都遇到過這個(gè)問題,有問題不害怕,害怕的是沒有問題。
事實(shí)上在這兩條數(shù)據(jù)中只有 20841 是真正的事務(wù) ID,那么第二條數(shù)據(jù)中的 ID 是什么呢!
想知道這個(gè)數(shù)字是什么的前提是知道是怎么來的。
從上圖可以看出,當(dāng)執(zhí)行 select 語句后會(huì)產(chǎn)生一個(gè)非常大的事務(wù) ID,那能不能理解為這種差異非常大的事務(wù) ID 是通過快照讀的方式才會(huì)生成的。
接著再這個(gè)事務(wù)下面在執(zhí)行一個(gè) insert 語句,然后再查看一下事務(wù) ID 的狀態(tài)。
不可思議的是在事務(wù)中先執(zhí)行 select 語句,然后執(zhí)行 insert 語句,事務(wù) ID 發(fā)生了變化,這是什么原因呢?
經(jīng)過資料查詢得知當(dāng)執(zhí)行一個(gè)簡(jiǎn)單的 select 語句時(shí),被稱之為只讀事務(wù),為了避免給只讀事務(wù)分配 trx_id 帶來不必要的開銷就沒有對(duì)其分配事務(wù) ID。
只讀事務(wù)沒有分配 undo segment 也不會(huì)分配 LOCK 鎖結(jié)構(gòu),本質(zhì)上只讀事務(wù)的 trx_id 的值就是 0。
但是為了執(zhí)行 select * from information_schema.INNODB_TRX 或者 show engine innodb status 時(shí)。
就會(huì)通過 reinterpret_cast(trx) | (max_trx_id + 1) 將指針轉(zhuǎn)換為一個(gè) 64 字節(jié)非負(fù)整數(shù)然后位或 (max_trx_id + 1) 就是這么個(gè)值。
關(guān)于這個(gè)值的生成過程就不用再去深究了,只需要知道在只讀事務(wù)下是不會(huì)分配事務(wù) ID,而查詢出來的這個(gè)值只是為了顯示而存在的沒有實(shí)際意義。
但是當(dāng)你執(zhí)行 select * from information_schema.INNODB_TRX 查詢出來的事務(wù) ID,再通過 show engine innodb status 查詢是查不到的。
在 Innodb 下如果事務(wù)為只讀事務(wù)則不會(huì)在 Innodb 數(shù)據(jù)結(jié)構(gòu)中顯示,因此你是看不到的。
作者:咔咔
簡(jiǎn)介:堅(jiān)持學(xué)習(xí)、堅(jiān)持寫博、堅(jiān)持分享是咔咔從業(yè)以來一直所秉持的信念。希望在偌大互聯(lián)網(wǎng)中咔咔的文章能帶給你一絲絲幫助。我是咔咔,下期見。
編輯:陶家龍
征稿:有投稿、尋求報(bào)道意向技術(shù)人請(qǐng)?zhí)砑有【幬⑿?gordonlonglong
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】