最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MySQL中MVCC多版本并發(fā)控制

 更新時(shí)間:2026年06月29日 09:46:40   作者:伐塵  
多版本并發(fā)控制,通過管理記錄的多個(gè)版本,實(shí)現(xiàn)了數(shù)據(jù)庫(kù)事務(wù)并發(fā)時(shí)的一致性讀,即當(dāng)前事務(wù)讀取正在被其他事務(wù)更新的行時(shí),能讀到該記錄被更新之前的版本,下面就來介紹一下如何實(shí)現(xiàn),感興趣的可以了解一下

0. MySQL的ACID特性

每一個(gè)事務(wù)必須滿足下面的4個(gè)特性:

  • 事務(wù)的原子性(Atomic)
    事務(wù)是一個(gè)不可分割的整體,事務(wù)必須具有原子特性,及當(dāng)數(shù)據(jù)修改時(shí),要么全執(zhí)行,要么全不執(zhí)行,即不允許事務(wù)部分的完成。
  • 事務(wù)的一致性(Consistency)
    一個(gè)事務(wù)執(zhí)行之前和執(zhí)行之后,數(shù)據(jù)庫(kù)數(shù)據(jù)必須保持一致性狀態(tài)。數(shù)據(jù)庫(kù)的一致性狀態(tài)必須由用戶來負(fù)責(zé),由并發(fā)控制機(jī)制實(shí)現(xiàn)。就拿網(wǎng)上購(gòu)物來說,你只有讓商品出庫(kù),又讓商品進(jìn)入顧客的購(gòu)物車才能構(gòu)成一個(gè)完整的事務(wù)。
  • 事務(wù)的隔離性(Isolation)
    當(dāng)兩個(gè)或者多個(gè)事務(wù)并發(fā)執(zhí)行時(shí),為了保證數(shù)據(jù)的安全性,將一個(gè)事物內(nèi)部的操作與其它事務(wù)的操作隔離起來,不被其它正在執(zhí)行的事務(wù)所看到,使得并發(fā)執(zhí)行的各個(gè)事務(wù)之間不能互相影響。
  • 事務(wù)的持久性(Durability)
    事務(wù)完成(commit)以后,DBMS保證它對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)的修改是永久性的,即使數(shù)據(jù)庫(kù)因?yàn)楣收铣鲥e(cuò),也應(yīng)該能夠恢復(fù)數(shù)據(jù)!

1. 什么是MVCC

多版本并發(fā)控制,通過管理記錄的多個(gè)版本,實(shí)現(xiàn)了數(shù)據(jù)庫(kù)事務(wù)并發(fā)時(shí)的一致性讀,即當(dāng)前事務(wù)讀取正在被其他事務(wù)更新的行時(shí),能讀到該記錄被更新之前的版本。解決了讀寫沖突。
MVCC (Multiversion Concurrency Control),多版本并發(fā)控制。顧名思義,MVCC 是通過數(shù)據(jù)行的多個(gè)版本管理來實(shí)現(xiàn)數(shù)據(jù)庫(kù)的 并發(fā)控制。這項(xiàng)技術(shù)使得在InnoDB的事務(wù)隔離級(jí)別下執(zhí)行一致性讀操作有了保證。換言之,就是為了查詢一些正在被另一個(gè)事務(wù)更新的行,并且可以看到它們被更新之前的值,這樣在做查詢的時(shí)候就不用等待另一個(gè)事務(wù)釋放鎖。

快照讀與當(dāng)前讀

快照讀:MVCC在MySQLInnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫(kù)并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀,而這個(gè)讀指的就是快照讀,而非當(dāng)前讀。MVCC本質(zhì)是采用樂觀鎖思想的一種方式。
當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)。

2.1 快照讀

快照讀又叫一致性讀,讀取的是快照數(shù)據(jù)。不加鎖的簡(jiǎn)單的 SELECT 都屬于快照讀,即不加鎖的非阻塞讀;比如這樣:

SELECT * FROM player WHERE ...

之所以出現(xiàn)快照讀的情況,是基于提高并發(fā)性能的考慮,快照讀的實(shí)現(xiàn)是基于MVCC,它在很多情況下, 避免了加鎖操作,降低了開銷。
既然是基于多版本,那么快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。
快照讀的前提是隔離級(jí)別不是串行級(jí)別,串行級(jí)別下的快照讀會(huì)退化成當(dāng)前讀。

2.2 當(dāng)前讀

當(dāng)前讀讀取的是記錄的最新版本(最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)),讀取時(shí)還要保證其他并發(fā)事務(wù) 不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖。加鎖的 SELECT,或者對(duì)數(shù)據(jù)進(jìn)行增刪改都會(huì)進(jìn)行當(dāng)前 讀。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共享鎖
SELECT * FROM student FOR UPDATE; # 排他鎖
INSERT INTO student values ... # 排他鎖
DELETE FROM student WHERE ... # 排他鎖
UPDATE student SET ... # 排他鎖

3. MVCC三劍客

3.1 回顧隔離級(jí)別

事務(wù)隔離級(jí)別是指操作同一資源的并發(fā)事務(wù)之間的隔離度。隔離級(jí)別越高,事務(wù)之間的相互干擾就越小,安全性就越高。
讀問題:

  • 臟讀:讀到了臟數(shù)據(jù)。當(dāng)前事務(wù)讀到另一個(gè)未提交事務(wù)剛改的數(shù)據(jù)。只有讀未提交會(huì)臟讀。
  • 不可重復(fù)讀:前后重復(fù)讀的數(shù)據(jù)不一樣。前后兩次讀同數(shù)據(jù),這期間數(shù)據(jù)被其他事務(wù)改了,導(dǎo)致前后讀取的數(shù)據(jù)不同。
  • 幻讀:前后讀的數(shù)據(jù)是一樣,但多了幾行或少了幾行,像幻覺一樣。事務(wù)前后讀的數(shù)據(jù)集合不同,導(dǎo)致出現(xiàn)“幻像”行。僅串行化能解決幻讀問題。

事務(wù)隔離級(jí)別

  • 讀未提交:事務(wù)能讀到所有未提交事務(wù)的數(shù)據(jù)。壓根不加鎖、沒隔離,性能最高。
  • 讀提交:事務(wù)能讀到已提交事務(wù)的數(shù)據(jù)。底層由MVVC實(shí)現(xiàn),每次快照讀都生成讀視圖 。解決臟讀問題。
  • 可重復(fù)讀(默認(rèn)):前后讀的數(shù)據(jù)相同。底層由MVVC實(shí)現(xiàn),僅第一次快照讀生成讀視圖。解決臟讀、不可重復(fù)讀問題。MySQL InnoDB 引擎的可重復(fù)讀解決了幻讀問題。快照讀由MVCC解決,當(dāng)前讀通過 next-key lock解決。
  • 串行化:事務(wù)一拿到鎖阻塞其他事務(wù),直到釋放鎖。讀時(shí)共享鎖,寫時(shí)排它鎖。阻塞導(dǎo)致性能最差。解決臟讀、不可重復(fù)讀、幻讀問題。

MVVC:多版本并發(fā)控制。MVCC三劍客:隱藏字段、Undo Log、Read View。
共享鎖(讀鎖):在共享鎖下,多個(gè)線程可以同時(shí)讀取數(shù)據(jù),但只有一個(gè)線程能夠修改數(shù)據(jù)。當(dāng)一個(gè)線程在修改數(shù)據(jù)時(shí),必須獲得獨(dú)占鎖,以便其他線程不能訪問數(shù)據(jù)。
排它鎖(寫鎖):在排它鎖下,只有一個(gè)線程可以修改數(shù)據(jù),其他線程不允許訪問數(shù)據(jù)。
MVCC 可以不采用鎖機(jī)制,而是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀問題!它可以在大多數(shù)情況下替代行級(jí)鎖,降低系統(tǒng)的開銷

3.2 隱藏字段、Undo Log版本鏈

隱藏字段

對(duì)于使用 InnoDB 存儲(chǔ)引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列。

  • trx_id(事務(wù)id) :每次一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的 事務(wù)id 賦值給 trx_id 隱藏列。
  • roll_pointer(回滾指針) :指向undo日志里該記錄版本鏈的最近節(jié)點(diǎn)。每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入到 undo日志 中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過它來找到該記錄修改前的信息

Undo Log(回滾日志)

當(dāng)一個(gè)事務(wù)對(duì)數(shù)據(jù)庫(kù)執(zhí)行寫操作的時(shí)候,MySQL 會(huì)將要修改的數(shù)據(jù)的舊版本數(shù)據(jù)先寫入到 Undo Log 中,然后再將新版本數(shù)據(jù)寫入到數(shù)據(jù)庫(kù)中。在事務(wù)提交之前,Undo Log 中的數(shù)據(jù)可以用于回滾;在事務(wù)提交后,Undo Log 中的舊版本數(shù)據(jù)也可以被用來提供讀取操作的歷史版本數(shù)據(jù)。

Undo Log(回滾日志)的版本鏈

在 MySQL 的實(shí)現(xiàn)中,Undo Log 版本鏈用于維護(hù)數(shù)據(jù)的歷史版本,以及回滾和讀取歷史版本時(shí)用到的數(shù)據(jù)。

Undo Log 的版本鏈就是用于維護(hù)這些歷史版本數(shù)據(jù)的一個(gè)鏈表

在 Undo Log 版本鏈中,每個(gè)版本對(duì)應(yīng)一個(gè)版本號(hào)或時(shí)間戳,新版本數(shù)據(jù)會(huì)被添加到版本鏈的頭部。同時(shí),Undo Log 版本鏈也可以通過一個(gè)類似于鏈表的指針結(jié)構(gòu)來維護(hù)多個(gè)事務(wù)間的不同版本數(shù)據(jù)。

對(duì)于讀取操作,如果需要讀取歷史版本的數(shù)據(jù),MySQL 就可以通過訪問 Undo Log 版本鏈來獲取指定版本下的數(shù)據(jù)。而對(duì)于回滾操作,MySQL 可以簡(jiǎn)單地恢復(fù) Undo Log 中對(duì)應(yīng)版本的數(shù)據(jù),從

案例:
假設(shè)插入該記錄的事務(wù)id為8,那么此刻該條記錄的示意圖如下所示:

insert undo只在事務(wù)回滾時(shí)起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會(huì)被系統(tǒng)回收(也就是該undo日志占用的Undo頁(yè)面鏈表要么被重用,要么被釋放)。

假設(shè)之后兩個(gè)事務(wù)id分別為 10 、 20 的事務(wù)對(duì)這條記錄進(jìn)行UPDATE 操作,操作流程如下:

能不能在兩個(gè)事務(wù)中交叉更新同一條記錄呢?

不能!這不就是一個(gè)事務(wù)修改了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),臟寫。
lnnoDB使用鎖來保證不會(huì)有臟寫情況的發(fā)生,也就是在第一個(gè)事務(wù)更新了某條記錄后,就會(huì)給這條記錄加鎖,另一個(gè)事務(wù)再次更新時(shí)就需要等待第一個(gè)事務(wù)提交了,把鎖釋放之后才可以繼續(xù)更新。

每次對(duì)記錄進(jìn)行改動(dòng),都會(huì)記錄一條undo日志,每條undo日志也都有一個(gè) roll_pointer 屬性 ( INSERT 操作對(duì)應(yīng)的undo日志沒有該屬性,因?yàn)樵撚涗洸]有更早的版本),可以將這些 undo日志 都連起來,串成一個(gè)鏈表:

對(duì)該記錄每次更新后,都會(huì)將舊值放到一條 undo日志 中,就算是該記錄的一個(gè)舊版本,隨著更新次數(shù) 的增多,所有的版本都會(huì)被 roll_pointer 屬性連接成一個(gè)鏈表,我們把這個(gè)鏈表稱之為 版本鏈 ,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。
每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù)id。

3.3 ReadView

MVCC 的實(shí)現(xiàn)依賴于:隱藏字段、Undo Log、Read View。

3.3.1 ReadView(讀視圖)簡(jiǎn)約版

Read View是事務(wù)進(jìn)行快照讀操作的時(shí)候生產(chǎn)的讀視圖,在該事務(wù)執(zhí)行快照讀的那一刻,會(huì)生成一個(gè)數(shù)據(jù)系統(tǒng)當(dāng)前的快照,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的id,事務(wù)的id值是遞增的。
其實(shí)Read View的最大作用是用來做可見性判斷的,也就是說當(dāng)某個(gè)事務(wù)在執(zhí)行快照讀的時(shí)候,對(duì)該記錄創(chuàng)建個(gè)Read View的視圖,把這個(gè)讀視圖當(dāng)作條件去判斷當(dāng)前事務(wù)能夠看到哪個(gè)版本的數(shù)據(jù),有可能讀取到的是最新的數(shù)據(jù)也有可能讀取的是當(dāng)前行記錄的undolog中某個(gè)版本的數(shù)據(jù)
首先要知道Read View中的全局屬性:

  • creator_trx_id :創(chuàng)建這個(gè)讀視圖的事務(wù)的 ID。增刪改事務(wù)才有資格分配id,讀事務(wù)id為0.
  • trx_ids:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表 。
  • up_limit_id:記錄活躍事務(wù)列表中最小的事務(wù)ID。最小即最早。
  • low_limit_id:表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值。將是列表里最大id。
    Read View的規(guī)則,即可見性算法:
    通過讀視圖,可以判斷當(dāng)前查詢中,記錄的某個(gè)版本是否可見。

判斷方法是比較各版本的trx_id和讀視圖里的活躍事務(wù)id,如果某版本trx_id小于讀視圖的最小事務(wù)id,則代表那個(gè)版本是生成讀視圖之前的已提交版本,當(dāng)前查詢就可以訪問那個(gè)版本

MVCC流程:
查詢,生成讀視圖,用讀視圖的活躍事務(wù)id依次對(duì)比各版本的事務(wù)id,找到符合規(guī)則的數(shù)據(jù)。
應(yīng)用: 事務(wù)隔離級(jí)別里的讀提交和可重復(fù)讀底層是由MVCC實(shí)現(xiàn)的。并且MySQL InnoDB 引擎的可重復(fù)讀由MVCC解決了幻讀問題。
讀提交的MVCC原理:事務(wù)每次讀到的都是最新已提交的數(shù)據(jù)。每次讀取數(shù)據(jù)前都生成一ReadView??煺兆x生成Read View,不斷對(duì)比版本鏈各版本的trx_id,直到發(fā)現(xiàn)某版本trx_id比Read View的活躍事務(wù)列表里最小trx_id還小,該版本則是快照讀前最新已提交的數(shù)據(jù)。

可重復(fù)讀的MVCC原理:只在第一次查詢時(shí)生成ReadView,之后查詢用第一次快照讀時(shí)生成的ReadView。

在MVCC 機(jī)制中,多個(gè)事務(wù)對(duì)同一個(gè)行記錄進(jìn)行更新會(huì)產(chǎn)生多個(gè)歷史快照,這些歷史快照保存在 Undo Log 里。如果一個(gè)事務(wù)想要查詢這個(gè)行記錄,需要讀取哪個(gè)版本的行記錄呢? 這時(shí)就需要用到 Readview 了,它幫我們解決了行的可見性問題。
ReadView 就是事務(wù)在使用MVCC機(jī)制進(jìn)行快照讀操作時(shí)產(chǎn)生的讀視圖。當(dāng)事務(wù)啟動(dòng)時(shí),會(huì)生成數(shù)據(jù)庫(kù)系統(tǒng)當(dāng)前的個(gè)快照,lnnoDB 為每個(gè)事務(wù)構(gòu)造了一個(gè)數(shù)組,用來記錄并維護(hù)系統(tǒng)當(dāng)前 活躍事務(wù)的ID(“活躍”指的就是,啟動(dòng)了但還沒提交)。

3.3.2 設(shè)計(jì)思路

使用 READ UNCOMMITTED 隔離級(jí)別的事務(wù),由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了。

使用 SERIALIZABLE 隔離級(jí)別的事務(wù),InnoDB規(guī)定使用加鎖的方式來訪問記錄。

使用 READ COMMITTED 和 REPEATABLE READ 隔離級(jí)別的事務(wù),都必須保證讀到 已經(jīng)提交了的 事務(wù)修改過的記錄。假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見的,
這是ReadView要解決的主要問題。

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

說明:只有在對(duì)表中的記錄做改動(dòng)時(shí)(執(zhí)行INSERT、DELETE、UPDATE這些語(yǔ)句時(shí))才會(huì)為 事務(wù)分配事務(wù)id,否則在一個(gè)只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。

  • trx_ids ,表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的 事務(wù)id列表 。
  • up_limit_id ,活躍的事務(wù)中最小的事務(wù) ID。
  • low_limit_id ,表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值。low_limit_id 是系 統(tǒng)最大的事務(wù)id值,這里要注意是系統(tǒng)中的事務(wù)id,需要區(qū)別于正在活躍的事務(wù)ID。

注意:low_limit_id并不是trx_ids中的最大值,事務(wù)id是遞增分配的。比如,現(xiàn)在有id為1, 2,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成ReadView時(shí), trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

  • creator_trx_id :創(chuàng)建這個(gè)讀視圖的事務(wù)的 ID。增刪改事務(wù)才有資格分配id,讀事務(wù)id為0.
  • trx_ids:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表 。
  • up_limit_id:記錄活躍事務(wù)列表中最小的事務(wù)ID。最小即最早。
  • low_limit_id:表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值。將是列表里最大id。

3.3.3 ReadView的規(guī)則

有了這個(gè)ReadView,這樣在當(dāng)前事務(wù)訪問某條記錄時(shí),只需要按照下邊的步驟判斷記錄的某個(gè)版本是否可見。

  • 如果被訪問版本的trx_id屬性值與ReadView中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值小于ReadView中的 up_limit_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值大于或等于ReadView中的 low_limit_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
  • 如果被訪問版本的trx_id屬性值在ReadView的 up_limit_id 和 low_limit_id 之間,那就需要判斷一下trx_id屬性值是不是在 trx_ids 列表中。
    • 如果在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問。
    • 如果不在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。

3.3.4 MVCC整體操作流程

了解了這些概念之后,我們來看下當(dāng)查詢一條記錄的時(shí)候,系統(tǒng)如何通過MVCC找到它:

  1. 首先獲取事務(wù)自己的版本號(hào),也就是事務(wù) ID;
  2. 獲取 ReadView;
  3. 查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號(hào)進(jìn)行比較;
  4. 如果不符合 ReadView 規(guī)則,就需要從 Undo Log 中獲取歷史快照;
  5. 最后返回符合規(guī)則的數(shù)據(jù)。
    如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄。

InnoDB中,MVCC 是通過Undo Log + Read View 進(jìn)行數(shù)據(jù)讀取,Undo Log 保存了歷史快照,而 Read View 規(guī)則幫我們判斷當(dāng)前版本的數(shù)據(jù)是否可見。
在隔離級(jí)別為讀已提交(Read Committed)時(shí),一個(gè)事務(wù)中的每一次 SELECT 查詢都會(huì)重新獲取一次 Read View。

如表所示:

注意,此時(shí)同樣的查詢語(yǔ)句都會(huì)重新獲取一次 Read View,這時(shí)如果 Read View 不同,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況。

當(dāng)隔離級(jí)別為可重復(fù)讀的時(shí)候,就避免了不可重復(fù)讀,這是因?yàn)橐粋€(gè)事務(wù)只在第一次 SELECT 的時(shí)候會(huì)獲取一次 Read View,而后面所有的 SELECT 都會(huì)復(fù)用這個(gè) Read View,如下表所示:

4. 舉例說明MVCC流程

4.1 讀提交的MVCC流程

讀提交:事務(wù)每次讀到的都是最新已提交的數(shù)據(jù)。底層由MVVC實(shí)現(xiàn) ,每次讀取數(shù)據(jù)前都生成一個(gè)ReadView。只解決臟讀問題。

讀提交的MVCC原理:每次讀都生成Read View,不斷對(duì)比版本鏈各版本的trx_id,直到發(fā)現(xiàn)某版本trx_id比Read View的活躍事務(wù)列表里最小trx_id還小,該版本則是讀前最新已提交的數(shù)據(jù)。
現(xiàn)在有兩個(gè) 事務(wù)id 分別為 10 、 20 的事務(wù)在執(zhí)行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...

說明:事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(shí)(比如使用INSERT、DELETE、UPDATE語(yǔ)句),才會(huì)被分配一個(gè)單獨(dú)的事務(wù)id,這個(gè)事務(wù)id是遞增的。所以我們才在事務(wù)2中更新一些別的表的記錄,目的是讓它分配事務(wù)id。

此刻,表student 中 id 為 1 的記錄得到的版本鏈表如下所示:

假設(shè)現(xiàn)在有一個(gè)使用 讀提交READ COMMITTED 隔離級(jí)別的事務(wù)開始執(zhí)行:

# 使用READ COMMITTED隔離級(jí)別的事務(wù)
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三',對(duì)應(yīng)事務(wù)id為8

這個(gè) SELECT1 的執(zhí)行過程如下:

  1. 在執(zhí)行 SELECT 語(yǔ)句(快照讀)時(shí)會(huì)先生成一個(gè) ReadView。ReadView 的 trx_ids 列表的內(nèi)容就是[10,20]。up_limit_id為10,low_limit_id為21,creator_trx_id為0。
  2. 從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列 name 的內(nèi)容是王五,該版本的 trx_id 值為10,在trx_ids 列表內(nèi),所以不符合可見性要求,根據(jù) roll_pointer 跳到下一個(gè)版本。
  3. 下一個(gè)版本的列name 的內(nèi)容是,李四,該版本的 trx_id值也為 10,也在 trx_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
  4. 下一個(gè)版本的列name 的內(nèi)容是,張三,該版本的trx_id值為 8小于ReadView中的最小活躍事務(wù)up_limit_id值10,說明該版本是生成ReadView 之前的最近版本,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列 name 為,張三的記錄。
    之后,我們把 事務(wù)id 為 10 的事務(wù)提交一下:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到 事務(wù)id 為 20 的事務(wù)中更新一下表 student 中 id 為 1 的記錄:

# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE student SET name="錢七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 為 1 的記錄的版本鏈就長(zhǎng)這樣:

然后再到剛才使用 READ COMMITTED 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,如下:

# 使用READ COMMITTED隔離級(jí)別的事務(wù)
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'王五'

這個(gè) SELECT2 的執(zhí)行過程如下

  1. 在執(zhí)行 SELECT 語(yǔ)句時(shí)會(huì)又會(huì)單獨(dú)生成一個(gè) ReadView,該ReadView的 trx_ids 列表的內(nèi)容[20],就是up_limit_id為20,low_limit_id為21, creator_trx_id為0。
  2. 從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列 name 的內(nèi)容是,宋八,該版本的 trx_id 值為20,在 trx_ids 列表內(nèi),所以不符合可見性要求,根據(jù) roll_pointer 跳到下一個(gè)版本。
  3. 下一個(gè)版本的列name 的內(nèi)容是錢七’,該版本的 trx_id值為 28,也在 trx_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本
  4. 下一個(gè)版本的列name 的內(nèi)容是**'王五**’,該版本的trx_id值為10,小于ReadView 中的up_limit_id值28,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列 name 為,王五’的記錄。

4.2 可重復(fù)讀的MVCC流程

只在第一次查詢時(shí)生成ReadView,之后查詢用第一次快照讀時(shí)生成的ReadView。
比如,系統(tǒng)里有兩個(gè) 事務(wù)id 分別為 10 、 20 的事務(wù)在執(zhí)行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...

此刻,表student 中 id 為 1 的記錄得到的版本鏈表如下所示:

假設(shè)現(xiàn)在有一個(gè)使用 REPEATABLE READ 隔離級(jí)別的事務(wù)開始執(zhí)行:

# 使用REPEATABLE READ隔離級(jí)別的事務(wù)
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'

這個(gè)SELECT1 的執(zhí)行過程如下:

  1. 在執(zhí)行 SELECT 語(yǔ)句時(shí)會(huì)先生成一個(gè)Readview,ReadView的 trx_ids 列表的內(nèi)容就是[10,20].up_limit_id為10,low_limit_id為21,creator_trx_id為0。
  2. 然后從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列 name 的內(nèi)容是,王五’,該版本的 trx_id值為 10,在trx_ids 列表內(nèi),所以不符合可見性要求,根據(jù) rol1_pointer 跳到下一個(gè)版本.
  3. 下一個(gè)版本的列name 的內(nèi)容是,李四’,該版本的trx_id值也為 10,也在 trx_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
  4. 下一個(gè)版本的列name 的內(nèi)容是’張三’,該版本的trx_id值為 8,小于Readview 中的up_limit_id值10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列 name 為,張三’的記錄。

之后,我們把 事務(wù)id 為 10 的事務(wù)提交一下,就像這樣:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到 事務(wù)id 為 20 的事務(wù)中更新一下表 student 中 id 為 1 的記錄:

# Transaction 20
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE student SET name="錢七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student 中 id 為 1 的記錄的版本鏈長(zhǎng)這樣:


然后再到剛才使用 REPEATABLE READ 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè) id 為 1 的記錄,如下:

# 使用REPEATABLE READ隔離級(jí)別的事務(wù)
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值為'張三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍為'張三'

SELECT2 的執(zhí)行過程如下:

  1. 因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為 REPEATABLE READ,而之前在執(zhí)行SELECT1 時(shí)已經(jīng)生成過 ReadView了,所以此時(shí)直接復(fù)用之前的 ReadView,之前的 ReadView的 trx_ids 列表的內(nèi)容就是[10,20],up_limit_id為1, low_limit_id為21, creator_trx_id為0
  2. 然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是宋八’,該版本的trx_id值為29,在trx_ds列表內(nèi),所以不符合可見性要求,根據(jù)ro11 pointer 跳到下一個(gè)版本
  3. 下一個(gè)版本的列name 的內(nèi)容是’錢七’,該版本的 trx_id值為20,也在 trx_ids 列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
  4. 下一個(gè)版本的列name 的內(nèi)容是,王五’,該版本的trx_id值為10,而trx_ids 列表中是包含值為 10的事務(wù)id 的,所以該版本也不符合要求,同理下一個(gè)列 name 的內(nèi)容是,李四的版本也不符合要求。繼續(xù)跳到下個(gè)版本
  5. 下一個(gè)版本的列name 的內(nèi)容是’張三’,該版本的trx_id值為 80,小于 ReadView 中的up_limit_id值18,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列 c為,張三’的記錄。

這次SELECT查詢得到的結(jié)果是重復(fù)的,記錄的列c值都是張三,這就是可重復(fù)讀的含義。如果我們之后再把事務(wù)id為20的記錄提交了,然后再到剛才使用REPEATABLE READ隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記錄,得到的結(jié)果還是張三,具體執(zhí)行過程大家可以自己分析一下。

4.3 InnoDB 解決幻讀問題

接下來說明InnoDB 是如何解決幻讀的。
假設(shè)現(xiàn)在表 student 中只有一條數(shù)據(jù),數(shù)據(jù)內(nèi)容中,主鍵 id=1,隱藏的 trx_id=10,它的 undo log 如下圖所示。

假設(shè)現(xiàn)在有事務(wù) A 和事務(wù) B 并發(fā)執(zhí)行,事務(wù) A 的事務(wù) id 為 20 , 事務(wù) B 的事務(wù) id 為 30 。
步驟1:事務(wù) A 開始第一次查詢數(shù)據(jù),查詢的 SQL 語(yǔ)句如下。

select * from student where id >= 1;

在開始查詢之前,MySQL 會(huì)為事務(wù) A 產(chǎn)生一個(gè) ReadView,此時(shí) ReadView 的內(nèi)容如下: trx_ids= [20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。
由于此時(shí)表 student 中只有一條數(shù)據(jù),且符合 where id>=1 條件,因此會(huì)查詢出來。然后根據(jù) ReadView 機(jī)制,發(fā)現(xiàn)該行數(shù)據(jù)的trx_id=10,小于事務(wù) A 的 ReadView 里 up_limit_id,這表示這條數(shù)據(jù)是事務(wù) A 開啟之前,其他事務(wù)就已經(jīng)提交了的數(shù)據(jù),因此事務(wù) A 可以讀取到。
結(jié)論:事務(wù) A 的第一次查詢,能讀取到一條數(shù)據(jù),id=1。

步驟2:接著事務(wù) B(trx_id=30),往表 student 中新插入兩條數(shù)據(jù),并提交事務(wù)。

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');

此時(shí)表student 中就有三條數(shù)據(jù)了,對(duì)應(yīng)的 undo 如下圖所示:

步驟3**:接著事務(wù) A 開啟第二次查詢**,根據(jù)可重復(fù)讀隔離級(jí)別的規(guī)則,此時(shí)事務(wù) A 并不會(huì)再重新生成 ReadView。此時(shí)表 student 中的 3 條數(shù)據(jù)都滿足 where id>=1 的條件,因此會(huì)先查出來。然后根據(jù) ReadView 機(jī)制,判斷每條數(shù)據(jù)是不是都可以被事務(wù) A 看到。

  • 1)首先 id=1 的這條數(shù)據(jù),前面已經(jīng)說過了,可以被事務(wù) A 看到。
  • 2)然后是 id=2 的數(shù)據(jù),它的 trx_id=30,此時(shí)事務(wù) A 發(fā)現(xiàn),這個(gè)值處于 up_limit_id 和 low_limit_id 之 間,因此還需要再判斷 30 是否處于 trx_ids 數(shù)組內(nèi)。由于事務(wù) A 的 trx_ids=[20,30],因此在數(shù)組內(nèi),這表 示 id=2 的這條數(shù)據(jù)是與事務(wù) A 在同一時(shí)刻啟動(dòng)的其他事務(wù)提交的,所以這條數(shù)據(jù)不能讓事務(wù) A 看到。
  • 3)同理,id=3 的這條數(shù)據(jù),trx_id 也為 30,因此也不能被事務(wù) A 看見。

結(jié)論:最終事務(wù) A 的第二次查詢,只能查詢出 id=1 的這條數(shù)據(jù)。這和事務(wù) A 的第一次查詢的結(jié)果是一樣 的,因此沒有出現(xiàn)幻讀現(xiàn)象,所以說在 MySQL 的可重復(fù)讀隔離級(jí)別下,不存在幻讀問題。

5. 總結(jié)

這里介紹了 MVCC 在 READ COMMITTD 、 REPEATABLE READ 這兩種隔離級(jí)別的事務(wù)在執(zhí)行快照讀操作時(shí) 訪問記錄的版本鏈的過程。這樣使不同事務(wù)的 讀-寫 、 寫-讀 操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。

核心點(diǎn)在于 ReadView 的原理, READ COMMITTD 、 REPEATABLE READ 這兩個(gè)隔離級(jí)別的一個(gè)很大不同 就是生成ReadView的時(shí)機(jī)不同:

  • READ COMMITTD 在每一次進(jìn)行普通SELECT操作前都會(huì)生成一個(gè)ReadView
  • REPEATABLE READ 只在第一次進(jìn)行普通SELECT操作前生成一個(gè)ReadView,之后的查詢操作都重復(fù) 使用這個(gè)ReadView就好了。
    說明: 我們之前說執(zhí)行DELETE語(yǔ)句或者更新主鍵的UPDATE語(yǔ)句并不會(huì)立即把對(duì)應(yīng)的記錄完全從頁(yè)面中刪除而是執(zhí)行一個(gè)所謂的delete mark操作,相當(dāng)于只是對(duì)記錄打上了一個(gè)刪除標(biāo)志位,這主要就是為MVCC服務(wù)的。

通過MVCC我們可以解決:

  • 讀寫之間阻塞的問題。通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務(wù)并發(fā)處理能力。
  • 降低了死鎖的概率。這是因?yàn)?MVCC 采用了樂觀鎖的方式,讀取數(shù)據(jù)時(shí)并不需要加鎖,對(duì)于寫操作,也只鎖定必要的行。
  • 解決快照讀的問題。當(dāng)我們查詢數(shù)據(jù)庫(kù)在某個(gè)時(shí)間點(diǎn)的快照時(shí),只能看到這個(gè)時(shí)間點(diǎn)之前事務(wù)提交更新的結(jié)3果,而不能看到這個(gè)時(shí)間點(diǎn)之后事務(wù)提交的更新結(jié)果.

到此這篇關(guān)于MySQL中MVCC多版本并發(fā)控制的文章就介紹到這了,更多相關(guān)MySQL MVCC多版本并發(fā)控制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • MySQL解決Navicat設(shè)置默認(rèn)字符串時(shí)的報(bào)錯(cuò)問題

    MySQL解決Navicat設(shè)置默認(rèn)字符串時(shí)的報(bào)錯(cuò)問題

    本文主要介紹了MySQL解決Navicat設(shè)置默認(rèn)字符串時(shí)的報(bào)錯(cuò),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • MySQL Slave 觸發(fā) oom-killer解決方法

    MySQL Slave 觸發(fā) oom-killer解決方法

    這篇文章主要介紹了MySQL Slave 觸發(fā) oom-killer解決方法,需要的朋友可以參考下
    2016-07-07
  • mysql使用force index的問題解決

    mysql使用force index的問題解決

    FORCE INDEX是MySQL中的一個(gè)查詢提示,本文主要介紹了mysql使用force index的問題解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-07-07
  • 用VirtualBox構(gòu)建MySQL測(cè)試環(huán)境的筆記

    用VirtualBox構(gòu)建MySQL測(cè)試環(huán)境的筆記

    這篇文章主要介紹了如何用VirtualBox構(gòu)建MySQL測(cè)試環(huán)境,特分享下,方便需要的朋友
    2013-08-08
  • MySQL中去重處理的方法小結(jié)

    MySQL中去重處理的方法小結(jié)

    本文主要介紹了MySQL中去重方法小結(jié),包含DISTINCT、GROUPBY、聚合函數(shù)、子查詢、臨時(shí)表、窗口函數(shù)、GROUP_CONCAT這些方法,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-11-11
  • 解決“無(wú)法啟動(dòng)mysql服務(wù) 錯(cuò)誤1069”的方法

    解決“無(wú)法啟動(dòng)mysql服務(wù) 錯(cuò)誤1069”的方法

    本文給大家分享的是小編解決自己網(wǎng)站無(wú)法連接數(shù)據(jù)庫(kù)的時(shí)候遇到的“無(wú)法啟動(dòng)mysql服務(wù) 錯(cuò)誤1069”的方案,有相同需求的小伙伴可以參考下
    2017-08-08
  • 用SELECT... INTO OUTFILE語(yǔ)句導(dǎo)出MySQL數(shù)據(jù)的教程

    用SELECT... INTO OUTFILE語(yǔ)句導(dǎo)出MySQL數(shù)據(jù)的教程

    這篇文章主要介紹了用SELECT... INTO OUTFILE語(yǔ)句導(dǎo)出MySQL數(shù)據(jù)的教程,是MySQL入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下
    2015-05-05
  • Canal實(shí)現(xiàn)MYSQL實(shí)時(shí)數(shù)據(jù)同步的示例代碼

    Canal實(shí)現(xiàn)MYSQL實(shí)時(shí)數(shù)據(jù)同步的示例代碼

    本文詳細(xì)介紹了Canal部署的全過程,包括Canal-Admin、Canal-Server和Canal-Adapter的安裝和配置,涵蓋創(chuàng)建目錄、修改配置文件、容器部署等步驟,適用于MYSQL8.0+環(huán)境,旨在幫助用戶實(shí)現(xiàn)MYSQL實(shí)時(shí)數(shù)據(jù)同步
    2024-11-11
  • Mysql 5.7.9 shutdown 語(yǔ)法實(shí)例詳解

    Mysql 5.7.9 shutdown 語(yǔ)法實(shí)例詳解

    之前如果想關(guān)閉一個(gè)mysql數(shù)據(jù)庫(kù)可以通過kill 命令、mysqladmin shutdown 、service mysqld stop 等這樣的方式。然而在mysql-5.7.9之后mysql終于提供了SQL接口的shutdown語(yǔ)法啦
    2017-06-06
  • MySQL 日期格式化的使用示例

    MySQL 日期格式化的使用示例

    在MySQL中,可以使用DATE_FORMAT函數(shù)對(duì)日期進(jìn)行格式化,本文就來介紹一下MySQL 日期格式化的使用示例,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-10-10

最新評(píng)論

平远县| 阿鲁科尔沁旗| 同江市| 大邑县| 城固县| 容城县| 铜山县| 遂昌县| 娱乐| 永昌县| 疏附县| 喜德县| 玉林市| 磴口县| 临高县| 镶黄旗| 偃师市| 东平县| 宣汉县| 博湖县| 沙河市| 唐河县| 盐源县| 陕西省| 太谷县| 扶沟县| 兴安盟| 甘谷县| 奉新县| 大新县| 资溪县| 克拉玛依市| 平利县| 西昌市| 禄劝| 故城县| 徐水县| 东丽区| 循化| 阳朔县| 韶山市|