溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

mysql幻讀指的是什么

發(fā)布時間:2023-05-10 10:25:20 來源:億速云 閱讀:241 作者:iii 欄目:MySQL數(shù)據(jù)庫

本篇內(nèi)容主要講解“mysql幻讀指的是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“mysql幻讀指的是什么”吧!

在mysql中,幻讀指當(dāng)用戶讀取某一范圍的數(shù)據(jù)行時,另一個事務(wù)又在該范圍內(nèi)插入了新行,當(dāng)用戶再讀取該范圍的數(shù)據(jù)行時,會發(fā)現(xiàn)有新的“幻影”行。所謂的幻讀,就是通過SELECT查詢出來的數(shù)據(jù)集并不是真實存在的數(shù)據(jù)集,用戶通過SELECT語句查詢出某條記錄是不存在的,但是它有可能在真實的表中是存在的。

什么是幻讀

先來看看事務(wù)的隔離級別
mysql幻讀指的是什么
然后,談幻讀之前,我先說說我對幻讀的理解:

所謂幻讀,重點在于“幻”這個詞,很夢幻,很玄乎,真假不定,就像蒙上了一層霧一樣,你不能真真切切的看到對方,給人以幻的感覺,這便是“幻”。而所謂的幻讀,也就是你通過SELECT查詢出來的數(shù)據(jù)集并不是真實存在的數(shù)據(jù)集,你通過SELECT語句查詢出某條記錄是不存在的,但是它有可能在真實的表中是存在的。

我是這么理解幻讀與不可重復(fù)讀的:

  • 幻讀說的是存不存在的問題:原來不存在的,現(xiàn)在存在了,則是幻讀

  • 不可重復(fù)讀說的是變沒變化的問題:原來是A,現(xiàn)在卻變?yōu)榱薆,則為不可重復(fù)讀


幻讀,目前我了解的有兩種說法:

說法一:事務(wù) A 根據(jù)條件查詢得到了 N 條數(shù)據(jù),但此時事務(wù) B 刪除或者增加了 M 條符合事務(wù) A 查詢條件的數(shù)據(jù),這樣當(dāng)事務(wù) A 再次進(jìn)行查詢的時候真實的數(shù)據(jù)集已經(jīng)發(fā)生了變化,但是A卻查詢不出來這種變化,因此產(chǎn)生了幻讀。

這一種說法強調(diào)幻讀在于某一個范圍內(nèi)的數(shù)據(jù)行變多或者是變少了,側(cè)重說明的是數(shù)據(jù)集不一樣導(dǎo)致了產(chǎn)生了幻讀。

說法二:幻讀并不是說兩次讀取獲取的結(jié)果集不同,幻讀側(cè)重的方面是某一次的 select 操作得到的結(jié)果所表征的數(shù)據(jù)狀態(tài)無法支撐后續(xù)的業(yè)務(wù)操作。更為具體一些:A事務(wù)select 某記錄是否存在,結(jié)果為不存在,準(zhǔn)備插入此記錄,但執(zhí)行 insert 時發(fā)現(xiàn)此記錄已存在,無法插入,此時就發(fā)生了幻讀。產(chǎn)生這樣的原因是因為有另一個事務(wù)往表中插入了數(shù)據(jù)。


我個人更贊成第一種說法。

說法二這種情況也屬于幻讀,說法二歸根到底還是數(shù)據(jù)集發(fā)生了改變,查詢得到的數(shù)據(jù)集與真實的數(shù)據(jù)集不匹配。

對于說法二:當(dāng)進(jìn)行INSERT的時候,也需要隱式的讀取,比如插入數(shù)據(jù)時需要讀取有沒有主鍵沖突,然后再決定是否能執(zhí)行插入。如果這時發(fā)現(xiàn)已經(jīng)有這個記錄了,就沒法插入。所以,SELECT 顯示不存在,但是INSERT的時候發(fā)現(xiàn)已存在,說明符合條件的數(shù)據(jù)行發(fā)生了變化,也就是幻讀的情況,而不可重復(fù)讀指的是同一條記錄的內(nèi)容被修改了。

舉例來說明:說法二說的是如下的情況:
有兩個事務(wù)A和B,A事務(wù)先開啟,然后A開始查詢數(shù)據(jù)集中有沒有id = 30的數(shù)據(jù),查詢的結(jié)果顯示數(shù)據(jù)中沒有id = 30的數(shù)據(jù)。緊接著又有一個事務(wù)B開啟了,B事務(wù)往表中插入了一條id = 30的數(shù)據(jù),然后提交了事務(wù)。然后A再開始往表中插入id = 30的數(shù)據(jù),由于B事務(wù)已經(jīng)插入了id = 30的數(shù)據(jù),自然是不能插入,緊接著A又查詢了一次,結(jié)果發(fā)現(xiàn)表中沒有id = 30的數(shù)據(jù)呀,A事務(wù)就很納悶了,怎么會插入不了數(shù)據(jù)呢。當(dāng)A事務(wù)提交以后,再次查詢,發(fā)現(xiàn)表中的確存在id = 30的數(shù)據(jù)。但是A事務(wù)還沒提交的時候,卻查不出來?
其實,這便是可重復(fù)讀的作用。

過程如下圖所示:

mysql幻讀指的是什么

上圖中操作的t表的創(chuàng)建語句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`) -- 創(chuàng)建索引
) ENGINE=InnoDB;

INSERT INTO t VALUES(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

MySQL使用的InnoDB引擎默認(rèn)的隔離級別是可重復(fù)讀,也就是說在同一個事務(wù)中,兩次執(zhí)行同樣的查詢,得到的效果應(yīng)該是一樣的。因此,盡管B事務(wù)在A事務(wù)還未結(jié)束的時候,增加了表中的數(shù)據(jù),但是為了維護(hù)可重復(fù)讀,A事務(wù)中不管怎么查詢,是查詢不了新增的數(shù)據(jù)的。但是對于真實的表而言,表中的數(shù)據(jù)是的的確確增加了。

A查詢不到這個數(shù)據(jù),不代表這個數(shù)據(jù)不存在查詢得到了某條數(shù)據(jù),不代表它真的存在。這樣是是而非的查詢,就像是幻覺一樣,似真似假,故為幻讀。
產(chǎn)生幻讀的原因歸根到底是由于查詢得到的結(jié)果與真實的結(jié)果不匹配。

幻讀 VS 不可重復(fù)讀

  • 幻讀重點在于數(shù)據(jù)是否存在。原本不存在的數(shù)據(jù)卻真實的存在了,這便是幻讀。在同一個事務(wù)中,第一次讀取到結(jié)果集和第二次讀取到的結(jié)果集不同。(對比上面的例子,當(dāng)B事務(wù)INSERT以后,A事務(wù)中再進(jìn)行插入,此次插入相當(dāng)于一次隱式查詢)。引起幻讀的原因在于另一個事務(wù)進(jìn)行了INSERT操作。

  • 不可重復(fù)讀重點在于數(shù)據(jù)是否被改變了。在一個事務(wù)中對同一條記錄進(jìn)行查詢,第一次讀取到的數(shù)據(jù)和第二次讀取到的數(shù)據(jù)不一致,這便是可重復(fù)讀。引起不可重復(fù)讀的原因在于另一個事務(wù)進(jìn)行了UPDATE或者是DELETE操作。

簡單來說:幻讀是說數(shù)據(jù)的條數(shù)發(fā)生了變化,原本不存在的數(shù)據(jù)存在了。不可重復(fù)讀是說數(shù)據(jù)的內(nèi)容發(fā)生了變化,原本存在的數(shù)據(jù)的內(nèi)容發(fā)生了改變

可重復(fù)讀隔離下為什么會產(chǎn)生幻讀?

在可重復(fù)讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務(wù)插入的數(shù)據(jù)的。因此,幻讀在 當(dāng)前讀 下才會出現(xiàn)。

什么是快照讀,什么是當(dāng)前讀?

快照讀讀取的是快照數(shù)據(jù)。不加鎖的簡單的 SELECT都屬于快照讀,比如這樣:

SELECT * FROM player WHERE ...

當(dāng)前讀就是讀取最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)。加鎖SELECT,或者對數(shù)據(jù)進(jìn)行增刪改都會進(jìn)行當(dāng)前讀。這有點像是 Java 中的 volatile 關(guān)鍵字,被 volatile 修飾的變量,進(jìn)行修改時,JVM 會強制將其寫回內(nèi)存,而不是放在 CPU 緩存中,進(jìn)行讀取時,JVM 會強制從內(nèi)存讀取,而不是放在 CPU 緩存中。這樣就能保證其可見行,保證每次讀取到的都是最新的值。如果沒有用 volatile 關(guān)鍵字修飾,變量的值可能會被放在 CPU 緩存中,這就導(dǎo)致讀取到的值可能是某次修改的值,不能保證是最新的值。

說多了,我們繼續(xù)來看,如下的操作都會進(jìn)行 當(dāng)前讀。

SELECT * FROM player LOCK IN SHARE MODE;
SELECT * FROM player FOR UPDATE;
INSERT INTO player values ...
DELETE FROM player WHERE ...
UPDATE player SET ...

說白了,快照讀就是普通的讀操作,而當(dāng)前讀包括了 加鎖的讀取DML(DML只是對表內(nèi)部的數(shù)據(jù)操作,不涉及表的定義,結(jié)構(gòu)的修改。主要包括insert、update、deletet) 操作。

比如在可重復(fù)讀的隔離條件下,我開啟了兩個事務(wù),在另一個事務(wù)中進(jìn)行了插入操作,當(dāng)前事務(wù)如果使用當(dāng)前讀 是可以讀到最新的數(shù)據(jù)的。

mysql幻讀指的是什么

MySQL中如何實現(xiàn)可重復(fù)讀

當(dāng)隔離級別為可重復(fù)讀的時候,事務(wù)只在第一次 SELECT 的時候會獲取一次 Read View,而后面所有的 SELECT 都會復(fù)用這個 Read View。也就是說:對于A事務(wù)而言,不管其他事務(wù)怎么修改數(shù)據(jù),對于A事務(wù)而言,它能看到的數(shù)據(jù)永遠(yuǎn)都是第一次SELECT時看到的數(shù)據(jù)。這顯然不合理,如果其它事務(wù)插入了數(shù)據(jù),A事務(wù)卻只能看到過去的數(shù)據(jù),讀取不了當(dāng)前的數(shù)據(jù)。

既然都說到 Read View 了,就不得不說 MVCC (多版本并發(fā)控制) 機制了。MVCC 其實字面意思還比較好理解,為了防止數(shù)據(jù)產(chǎn)生沖突,我們可以使用時間戳之類的來進(jìn)行標(biāo)識,不同的時間戳對應(yīng)著不同的版本。比如你現(xiàn)在有1000元,你借給了張三 500 元, 之后李四給了你 500 元,雖然你的錢的總額都是 1000元,但是其實已經(jīng)和最開始的 1000元不一樣了,為了判斷中途是否有修改,我們就可以采用版本號來區(qū)分你的錢的變動。

如下,在數(shù)據(jù)庫的數(shù)據(jù)表中,id,name,type 這三個字段是我自己建立的,但是除了這些字段,其實還有些隱藏字段是 MySQL 偷偷為我們添加的,我們通常是看不到這樣的隱藏字段的。

mysql幻讀指的是什么

我們重點關(guān)注這兩個隱藏的字段:

  • db_trx_id:操作這行數(shù)據(jù)的事務(wù) ID,也就是最后一個對該數(shù)據(jù)進(jìn)行插入或更新的事務(wù) ID。我們每開啟一個事務(wù),都會從數(shù)據(jù)庫中獲得一個事務(wù) ID(也就是事務(wù)版本號),這個事務(wù) ID 是自增長的,通過 ID 大小,我們就可以判斷事務(wù)的時間順序。

  • db_roll_ptr:回滾指針,指向這個記錄的 Undo Log 信息。什么是 Undo Log 呢?可以這么理解,當(dāng)我們需要修改某條記錄時,MySQL 擔(dān)心以后可能會撤銷該修改,回退到之前的狀態(tài),所以在修改之前,先把當(dāng)前的數(shù)據(jù)存?zhèn)€檔,然后再進(jìn)行修改,Undo Log 就可以理解為是這個存檔文件。這就像是我們打游戲一樣,打到某個關(guān)卡先存?zhèn)€檔,然后繼續(xù)往下一關(guān)挑戰(zhàn),如果下一關(guān)挑戰(zhàn)失敗,就回到之前的存檔點,不至于從頭開始。

在 MVCC(多版本并發(fā)控制) 機制中,多個事務(wù)對同一個行記錄進(jìn)行更新會產(chǎn)生多個歷史快照,這些歷史快照保存在 Undo Log 里。如下圖所示,當(dāng)前行記錄的 回滾指針 指向的是它的上一個狀態(tài),它的上一個狀態(tài)的 回滾指針 又指向了上一個狀態(tài)的上一個狀態(tài)。這樣,理論上我們通過遍歷 回滾指針,就能找到該行數(shù)據(jù)的任意一個狀態(tài)。

Undo Log 示意圖

mysql幻讀指的是什么

我們沒有想到,我們看到的或許只是一條數(shù)據(jù),但是MySQL卻在背后為該條數(shù)據(jù)存儲多個版本,為這條數(shù)據(jù)存了非常多的檔。那問題來了,當(dāng)我們開啟事務(wù)時,我們在事務(wù)中想要查詢某條數(shù)據(jù),但是每一條數(shù)據(jù),都對應(yīng)了非常多的版本,這時,我們需要讀取哪個版本的行記錄呢?

這時就需要用到 Read View 機制了,它幫我們解決了行的可見性問題。Read View 保存了當(dāng)前事務(wù)開啟時所有活躍(還沒有提交)的事務(wù)列表。

在 Read VIew 中有幾個重要的屬性:

  • trx_ids,系統(tǒng)當(dāng)前正在活躍的事務(wù) ID 集合

  • low_limit_id,活躍的事務(wù)中最大的事務(wù) ID

  • up_limit_id,活躍的事務(wù)中最小的事務(wù) ID

  • creator_trx_id,創(chuàng)建這個 Read View 的事務(wù) ID

在前面我們說過了,在每一行記錄中有一個隱藏字段 db_trx_id,表示操作這行數(shù)據(jù)的事務(wù) ID ,而且 事務(wù) ID 是自增長的,通過 ID 大小,我們就可以判斷事務(wù)的時間順序。

當(dāng)我們開啟事務(wù)以后,準(zhǔn)備查詢某條記錄,發(fā)現(xiàn)該條記錄的 db_trx_id < up_limit_id,這說明什么呢?說明該條記錄一定是在本次事務(wù)開啟之前就已經(jīng)提交的,對于當(dāng)前事務(wù)而言,這屬于歷史數(shù)據(jù),可見,因此,我們通過 select 一定能查出這一條記錄。

但是如果發(fā)現(xiàn),要查詢的這條記錄的 db_trx_id > up_limit_id。這說明什么呢,說明我在開啟事務(wù)的時候,這條記錄肯定是還沒有的,是在之后這條記錄才被創(chuàng)建的,不應(yīng)該被當(dāng)前事務(wù)看見,這時候我們就可以通過 回滾指針 + Undo Log 去找一下該記錄的歷史版本,返回給當(dāng)前事務(wù)。在本文 什么是幻讀 ? 這一章節(jié)中舉的一個例子。A 事務(wù)開啟時,數(shù)據(jù)庫中還沒有(30, 30, 30)這條記錄。A事務(wù)開啟以后,B事務(wù)往數(shù)據(jù)庫中插入了(30, 30, 30)這條記錄,這時候,A事務(wù)使用 不加鎖select 進(jìn)行 快照讀 時是查詢不出這條新插入的記錄的,這符合我們的預(yù)期。對于 A事務(wù)而言,(30, 30, 30)這條記錄的 db_trx_id 一定大于 A事務(wù)開啟時的 up_limit_id,所以這條記錄不應(yīng)該被A事務(wù)看見。

如果需要查詢的這條記錄的 trx_id 滿足 up_limit_id < trx_id < low_limit_id 這個條件,說明該行記錄所在的事務(wù) trx_id 在目前 creator_trx_id 這個事務(wù)創(chuàng)建的時候,可能還處于活躍的狀態(tài),因此我們需要在 trx_ids 集合中進(jìn)行遍歷,如果 trx_id 存在于 trx_ids 集合中,證明這個事務(wù) trx_id 還處于活躍狀態(tài),不可見,如果該記錄有 Undo Log,我們可以通過回滾指針進(jìn)行遍歷,查詢該記錄的歷史版本數(shù)據(jù)。如果 trx_id 不存在于 trx_ids 集合中,證明事務(wù) trx_id 已經(jīng)提交了,該行記錄可見。

從圖中你能看到回滾指針將數(shù)據(jù)行的所有快照記錄都通過鏈表的結(jié)構(gòu)串聯(lián)了起來,每個快照的記錄都保存了當(dāng)時的 db_trx_id,也是那個時間點操作這個數(shù)據(jù)的事務(wù) ID。這樣如果我們想要找歷史快照,就可以通過遍歷回滾指針的方式進(jìn)行查找。

最后,再來強調(diào)一遍:事務(wù)只在第一次 SELECT 的時候會獲取一次 Read View

因此,如下圖所示,在 可重復(fù)讀 的隔離條件下,在該事務(wù)中不管進(jìn)行多少次 以WHERE heigh > 2.08為條件 的查詢,最終結(jié)果得到都是一樣的,盡管可能會有其它事務(wù)對這個結(jié)果集進(jìn)行了更改。

mysql幻讀指的是什么

如何解決幻讀

即便是給每行數(shù)據(jù)都加上行鎖,也無法解決幻讀,行鎖只能阻止修改,無法阻止數(shù)據(jù)的刪除。而且新插入的數(shù)據(jù),自然是數(shù)據(jù)庫中不存在的數(shù)據(jù),原本不存在的數(shù)據(jù)自然無法對其加鎖,因此僅僅使用行鎖是無法阻止別的事務(wù)插入數(shù)據(jù)的。

為了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。比如文章開頭的表 t,初始化插入了 6 個記錄,這就產(chǎn)生了 7 個間隙。

表 t 主鍵索引上的行鎖和間隙鎖
mysql幻讀指的是什么
也就是說這時候,在一行行掃描的過程中,不僅將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖?,F(xiàn)在你知道了,數(shù)據(jù)行是可以加上鎖的實體,數(shù)據(jù)行之間的間隙,也是可以加上鎖的實體。但是間隙鎖跟我們之前碰到過的鎖都不太一樣。

  • 間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開后閉區(qū)間。也就是說,我們的表 t 初始化以后,如果用 SELECT * FEOM t FOR UPDATE要把整個表所有記錄鎖起來,就形成了 7 個 next-key lock,分別是 (負(fù)無窮,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, 正無窮]。

  • 間隙鎖是在可重復(fù)讀隔離級別下才會生效的

怎么加間隙鎖呢?使用寫鎖(又叫排它鎖,X鎖)時自動生效,也就是說我們執(zhí)行 SELECT * FEOM t FOR UPDATE時便會自動觸發(fā)間隙鎖。會給主鍵加上上圖所示的鎖。

如下圖所示,如果在事務(wù)A中執(zhí)行了SELECT * FROM t WHERE d = 5 FOR UPDATE以后,事務(wù)B則無法插入數(shù)據(jù)了,因此就避免了產(chǎn)生幻讀。

數(shù)據(jù)表的創(chuàng)建語句如下

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`) -- 創(chuàng)建索引
) ENGINE=InnoDB;

INSERT INTO t VALUES(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

需要注意的是,由于創(chuàng)建數(shù)據(jù)表的時候僅僅只在c字段上創(chuàng)建了索引,因此使用條件WHERE id = 5查找時是會掃描全表的。因此,SELECT * FROM t WHERE d = 5 FOR UPDATE實際上鎖住了整個表,如上圖所示,產(chǎn)生了七個間隙,這七個間隙都不允許數(shù)據(jù)的插入。

因此當(dāng)B想插入一條數(shù)據(jù)(1, 1, 1)時就會被阻塞住,因為它的主鍵位于位于(0, 5]這個區(qū)間,被禁止插入。

mysql幻讀指的是什么

還需要注意的一點是,間隙鎖和間隙鎖是不會產(chǎn)生沖突的。
讀鎖(又稱共享鎖,S鎖)和寫鎖會沖突,寫鎖和寫鎖也會產(chǎn)生沖突。但是間隙鎖和間隙鎖是不會產(chǎn)生沖突的

如下:
A事務(wù)對id = 5的數(shù)據(jù)加了讀鎖,B事務(wù)再對id = 5的數(shù)據(jù)加寫鎖則會失敗,若B事務(wù)加讀鎖則會成功。讀鎖和讀鎖可以兼容,讀鎖和寫鎖則不能兼容。

A事務(wù)對id = 5的數(shù)據(jù)加了寫鎖,B事務(wù)再對id = 5的數(shù)據(jù)加寫鎖則會失敗,若B事務(wù)加讀鎖同樣也會失敗。
mysql幻讀指的是什么
在加了間隙鎖以后,當(dāng)A事務(wù)開啟以后,并對(5, 10]這個區(qū)間加了間隙鎖,那么B事務(wù)則無法插入數(shù)據(jù)了。

mysql幻讀指的是什么
但是當(dāng)A事務(wù)對(5, 10]加了間隙鎖以后,B事務(wù)也可以對這個區(qū)間加間隙鎖。

間隙鎖的目的是阻止往這個區(qū)間插入數(shù)據(jù),因此A事務(wù)加了以后B事務(wù)繼續(xù)加間隙鎖,這并不矛盾。但是對于寫鎖和讀鎖就不一樣了。
寫鎖是不允許其它事務(wù)讀,也不允許寫,而讀鎖則是允許寫,語義上就存在沖突。自然無法同時加這兩個鎖。
而寫鎖和寫鎖也是,寫鎖不允許讀,也不允許寫,想想,A事務(wù)對數(shù)據(jù)加了寫鎖,就是完全不想讓其它事務(wù)操作該數(shù)據(jù),那其它數(shù)據(jù)若能為這個數(shù)據(jù)加寫鎖,就相當(dāng)于是對該數(shù)據(jù)實施了操作,違背了寫鎖的涵義,自然不被允許。

到此,相信大家對“mysql幻讀指的是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI