詳解MVCC以及儘可能解決幻讀的兩種方案

PgSheep發表於2024-05-23

MVCC

透過「版本鏈」來控制併發事務訪問同一個記錄時的行為

並行事務問題 + 隔離級別

幻讀:在一個事務內多次查詢某個符合查詢條件的「記錄數量」,如果出現前後兩次查詢到的記錄數量不一樣的情況,就意味著發生了「幻讀」現象。

  • 髒讀:讀到其他事務未提交的資料;

  • 不可重複讀:前後讀取的資料不一致;

  • 幻讀:前後讀取的記錄數量不一致。

四個隔離級別如下:

  • 讀未提交(*read uncommitted*,指一個事務還沒提交時,它做的變更就能被其他事務看到;

  • 讀提交(*read committed*,指一個事務提交之後,它做的變更才能被其他事務看到;

  • 可重複讀(*repeatable read*,指一個事務執行過程中看到的資料,一直跟這個事務啟動時看到的資料是一致的,MySQL InnoDB 引擎的預設隔離級別

  • 序列化(*serializable*;會對記錄加上讀寫鎖,在多個事務對這條記錄進行讀寫操作時,如果發生了讀寫衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行;

MySQL 在「可重複讀」隔離級別下,可以很大程度上避免幻讀現象的發生(注意是很大程度避免,並不是徹底避免). [因為序列化會消耗效能]

MySQL InnoDB 引擎的預設隔離級別雖然是「可重複讀」,但是它很大程度上避免幻讀現象(並不是完全解決了)

解決的方案有兩種:

【需要加上next-key-lock,也不能完全避免】Next-Key Locks只能鎖住已經存在的範圍,無法防止範圍外的新插入記錄對查詢結果產生影響。

  • 針對快照讀(普通 select 語句),是透過 MVCC 方式解決了幻讀,因為可重複讀隔離級別下,事務執行過程中看到的資料,一直跟這個事務啟動時看到的資料是一致的,即使中途有其他事務插入了一條資料,是查詢不出來這條資料的,所以就很好了避免幻讀問題。

  • 針對當前讀(select ... for update 等語句),是透過 next-key lock(記錄鎖+間隙鎖)方式解決了幻讀,因為當執行 select ... for update 語句的時候,會加上 next-key lock,如果有其他事務在 next-key lock 鎖範圍內插入了一條記錄,那麼這個插入語句就會被阻塞,無法成功插入,所以就很好了避免幻讀問題。

這四種隔離級別具體是=如何實現的呢?

  • 對於「讀未提交」隔離級別的事務來說,因為可以讀到未提交事務修改的資料,所以直接讀取最新的資料就好了;

  • 對於「序列化」隔離級別的事務來說,透過加讀寫鎖的方式來避免並行訪問;

  • 對於「讀提交」和「可重複讀」隔離級別的事務來說,它們是透過 Read View 來實現的,它們的區別在於建立 Read View 的時機不同,大家可以把 Read View 理解成一個資料快照,就像相機拍照那樣,定格某一時刻的風景。「讀提交」隔離級別是在「每個語句執行前」都會重新生成一個 Read View,而「可重複讀」隔離級別是「啟動事務時」生成一個 Read View,然後整個事務期間都在用這個 Read View。

mvcc和間隙鎖解決幻讀

mvcc只能快照讀下的不可重複讀和幻讀??

RR情況下一當前讀一快照讀也會導致不可重複??

全稱Multi-Version Concurrency Control,多版本併發控制。指維護一個資料的多個版本,使得讀寫操作沒有衝突快照讀為MySQL實現MVCC提供了一個非阻塞讀功能。MVCC的具體實現,還需要依賴於資料庫記錄中的三個隱式欄位、undo log日誌、readView

【隔離級別越高,事務併發性會變低,效率也會變低】

開啟事務

注意,執行「開始事務」命令,並不意味著啟動了事務。在 MySQL 有兩種開啟事務的命令,分別是:

  • 第一種:begin/start transaction 命令; 【第一條select才開啟事務】

  • 第二種:start transaction with consistent snapshot 命令;【馬上開啟事務】

當前讀:

(select ... for update 等語句)

【這是加了共享鎖的當前讀,可重複讀是不加鎖的,這裡的共享鎖不相容排他鎖,另一個事物後續不能再做寫操作了】

讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。對於我們日常的操作,如:

  • select...lock in share mode(共享鎖)。

  • select..…for update、update、insert、delete(排他鎖)都是一種當前讀

比如A事務讀取不到B事務提交的新資料,因為當前隔離級別是RR(可重複讀,不會讀取到髒資料,會幻讀),加上 select...lock in share mode 就可以讀取到(當前讀)。-----也就是幻讀了

【這裡能讀到最新的是因為:加了意向共享鎖之後會隱式事務提交,也就是裡面實際提交了一次事務,而不是什麼打破隔離級別啥的】

快照讀:

(普通select語句)

【某個時間點資料的快照】

簡單的select(不加鎖)就是快照讀,快照讀,讀取的是記錄資料的可見版本,有可能是歷史資料,不加鎖,是非阻塞讀

  • Read Committed:每次select,都生成一個快照讀

  • Repeatable Read:開啟事務後第一個select語句才是快照讀的地方。

  • Serializable:快照讀會退化為當前讀。【每次讀取操作都會加速】

InnoDB行格式

InnoDB行格式-COMPACT

記錄頭資訊:delete_mask(標記刪除) 、next_record(指向「記錄頭資訊」和「真實資料」之間)、record_type(記錄的型別)

MySQL-InnoDB行格式

  • 設定NOT NULL至少省1位元組(以位元組為單位分配8位)【也是按照列的順序逆序排列】

  • InnoDB按道理varchar小於255位元組變長欄位長度列表1位元組,超過的話2位元組。

    但是explain的key_len 固定取2位元組,畢竟 key_len 的目的只是為了告訴你索引查詢中用了哪些索引欄位。

變長欄位長度列表」中的資訊之所以要逆序存放,是因為這樣可以使得位置靠前的記錄的真實資料和資料對應的欄位長度資訊可以同時在一個 CPU Cache Line 中,這樣就可以提高 CPU Cache 的命中率。

同樣的道理, NULL 值列表的資訊也需要逆序存放。

MySQL 規定除了 TEXT、BLOBs 這種大物件型別之外,其他所有的列(不包括隱藏列和記錄頭資訊)佔用的位元組長度加起來不能超過 65535 個位元組

要保證所有欄位的長度 + 變長欄位位元組數列表所佔用的位元組數 + NULL值列表所佔用的位元組數 <= 65535【記錄頭資訊和隱藏欄位呢】

行溢位:頁的大小一般是 16KB,也就是 16384位元組,存不下65532位元組,所以會部分存在溢位頁,原始的留20位元組指向新的資料頁(溢位頁)

  • Compressed 和 Dynamic 這兩種格式採用完全的行溢位方式,記錄的真實資料處 不會儲存該列的一部分資料,只儲存 20 個位元組的指標來指向溢位頁。而實際的資料都儲存在溢位頁中。【一個資料頁存的下】

MVCC 實現原理:

【作用:快照讀時透過mvcc找到對應的版本】 對於delete、update的不會,因為他們是當前讀,不經過mvcc,所以才會有RR級別還會有幻讀的問題,所以需要begin;之後馬上執行當前讀來鎖住資料,不讓其他的插入導致幻讀。

【解決不可重複讀】

隱藏欄位

有三個隱藏(兩個或者三個)的欄位:

進行改動時會改變

undo log回滾日誌,在insert、update、delete的時候產生的便於資料回滾的日誌。

當insert的時候,產生的undo log日誌只在回滾時需要,在事務提交後,可被立即刪除。【因為插入只有一次,trx_id = 1;】

update、delete的時候,產生的undo log日誌不僅在回滾時需要,在快照讀時也需要,不會立即被刪除。

undo log 版本鏈:

undo log日誌會記錄原來的版本的資料,因為是透過undo log 日誌進行回滾的。

readview

是快照讀SQL執行時MVCC提供資料的依據

當前活躍的事務:未提交

如何確定返回哪一個版本 這是由read view決定返回 undo log 中的哪一個版本。

【creator_trx_id:建立者的事務id(當前事務id?),不是每次快照讀就生成一次,RC、RR】

RC隔離級別下,在事務中每一次執行快照讀時生成ReadView。 RR隔離級別下,在事務中第一次執行快照讀時生成ReadView,後續會複用。 【可重複讀,一個事務讀取的兩條資料應該是一樣的】這裡是不是有問題??? 第四個條件,並不能讀取已提交的事務啊,這不就幻讀了?

【所以RC不可重複讀就是因為每次生成的readview都是新的,會看到別的事務提交的內容】RR只有事務開始才更新readview,所以別人提交事務也不會更新他的m_ids。

【獲取哪個版本的資料:拿undolog的當前事務id和readview的四個欄位進行對比】

【m_ids的都是活躍的事務,即未提交

【RR級別僅在第一次執行快照讀生成readview,原來如此,readview的資料在readview被建立後就固定了、不會被更新,除非被新的readview覆蓋,難怪會出現條件3的情況(被後來的事務修改並提交事務)

工作原理

讀不到時,並不會讀取這個版本的記錄。而是沿著 undo log 鏈條往下找舊版本的記錄

readview一固定(讀到的資料就一樣),能夠讀取的版本鏈、事務id、他的範圍就確定下來了。所以後面再改,也會讀到正確的位置。

(1)當前事務id肯定讀

(2)當前事務開始之前提交的肯定是要讀的。

(3)要看隔離級別,RR肯定不能讀。如果是不可重複讀可以讀取到(每次快照讀生成一個readview)【透過readview的生成時期實現】隔離級別區別不在過濾的條件,而在於它生成的時機

RC:max_trx_id 小於unlog的一條版本,讀到的話說明是讀已提交。沒有讀到就是可重複讀。【每次快照讀重新整理因為他要讀取最新的資料,要重新整理readview】

比如time1、time2,time1讀的是A,要想在time2讀到最新的資料就不能用RR級別。【需要生成一個全新的readview,因為前面的readview已經固定死了】

規則已經定好了(優雅),事務與事務的區別在於readview什麼時候重新整理,程式碼擴充套件性好

MVCC➕Next-key-Lock 防止幻讀

InnoDB儲存引擎在 RR 級別下透過 MVCCNext-key Lock 來解決幻讀問題:兩種解決方案儘可能避免幻讀(超過間隙鎖範圍的控制不了):

1、執行普通 select,此時會以 MVCC 快照讀的方式讀取資料

快照讀的情況下,RR 隔離級別只會在事務開啟後的第一次查詢生成 Read View ,並使用至事務提交。所以在生成 Read View 之後其它事務所做的更新、插入記錄版本對當前事務並不可見,實現了可重複讀和防止快照讀下(如果是update這些當前讀就可以讀到別人提交的。。)的 “幻讀” 【藉助第一次快照讀時,只生成一次readview】

2、執行 select...for update/lock in share mode、insert、update、delete 等當前讀 (MySQL 裡除了普通查詢是快照讀,其他都是當前讀,比如 update、insert、delete)-----【儘量在開啟事務之後,馬上執行 select ... for update 這類當前讀的語句,因為它會對記錄加 next-key lock,從而避免其他事務插入一條新記錄。】 -----如果用的普通select不會加next-key-lock所以會出現幻讀。

當前讀下,讀取的都是最新的資料,如果其它事務有插入新的記錄,並且剛好在當前事務查詢範圍內,就會產生幻讀!InnoDB 使用 來防止這種情況。當執行當前讀時,會鎖定讀取到的記錄的同時,鎖定它們的間隙,防止其它事務在查詢範圍內插入資料。只要我不讓你插入,就不會發生幻讀。【next-key lock 是間隙鎖+記錄鎖的組合】前提事務開啟之後就執行當前讀

Innodb 引擎為了解決「可重複讀」隔離級別使用「當前讀」而造成的幻讀問題,就引出了間隙鎖。

比如:

事務 B 在執行插入語句的時候,判斷到插入的位置被事務 A 加了 next-key lock,於是事務 B 會生成一個插入意向鎖,同時進入等待狀態,直到事務 A 提交了事務。 這就避免了由於事務 B 插入新記錄而導致事務 A 發生幻讀的現象。

未完全解決幻讀的例子

兩個發生幻讀場景的例子。

第一個例子:對於快照讀, MVCC 並不能完全避免幻讀現象。因為當事務 A 更新了一條事務 B 插入(必須commit之後A才能更新這條資料,因為鎖??)的記錄,那麼事務 A 前後兩次查詢的記錄條目就不一樣了,所以就發生幻讀。 ----【先更新了把新資料的trx_id改成事務A的,就可以用B的readview倒反天罡的讀取事務A插入的新資料】---不update直接select就不行,因為trx_id事務id還沒透過update(當前讀)非法更改。

第二個例子:對於當前讀,如果事務開啟後,並沒有執行當前讀,而是先快照讀(導致有機可趁),然後這期間如果其他事務插入了一條記錄,那麼事務後續使用當前讀進行查詢的時候,就會發現兩次查詢的記錄條目就不一樣了,所以就發生幻讀。----- 【儘量在開啟事務之後,馬上執行 select ... for update 這類當前讀的語句,因為它會對記錄加 next-key lock,從而避免其他事務插入一條新記錄。】 鎖住就不能被其他的插入了

注意:

本文章是基於黑馬程式設計師b站網課和小林coding總結出的,在此感謝!

UPDATE 和 DELETE 語句:對於更新和刪除操作,InnoDB使用的是當前讀(Current Read),而不是一致性讀。當前讀會讀取最新的資料版本,並對讀取到的記錄加鎖,以確保資料的安全性和一致性。因此,UPDATE和DELETE語句不會生成ReadView。

相關文章