面試官:undo log 是如何實現(xiàn) MVCC 的?
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
推薦:https://www.xttblog.com/?p=5319

看過我歷史文章的網(wǎng)友都知道,MySQL 有多種日志文件。其中 undo log 日志,經(jīng)常會在面試過程中被問到,今天我們一起來聊聊它!
undo log 日志涉及到日志回滾,所以非常重要。建議大家收藏起來,慢慢的細品!
事務
多線程并發(fā)執(zhí)行多個事務
說到事務,我相信大家都不陌生。都被它傷害過,而且還時不時的在面試中遇到事務失效的問題!
現(xiàn)在是一個多核時代,程序都充分的利用著多核 CPU。對于我們的業(yè)務系統(tǒng)去訪問數(shù)據(jù)庫而言,他往往都是多個線程并發(fā)的去執(zhí)行多個事務的。對于數(shù)據(jù)庫而言,它會有多個事務同時執(zhí)行,可能這多個事務還會同時更新和查詢同一條數(shù)據(jù)。那么如果是你來設計,你會如何來做?
MySQL 的開發(fā)者在設計之初就想到了這個問題,通過 undo log 等一系列巧妙的設計,讓事務的功能得意完美的實現(xiàn)。

每個事務都會執(zhí)行各種各樣的增刪改查語句,MySQL 底層會把磁盤上的數(shù)據(jù)頁加載到 buffer pool 的緩存頁里來,然后更新緩存頁,記錄 redo log 和 undo log,最終提交事務或者是回滾事務,多個事務會并發(fā)干上述一系列事情。
多客戶端、多線程并發(fā)的執(zhí)行多個事務,一般會帶來的下面幾個問題:
臟寫
事務 A 和事務 B 同時在更新一條數(shù)據(jù),事務 A 先把他更新為 A 值,事務 B 緊接著就把他更新為B 值。 此時事務 A 突然回滾了,那么就會用他的 undo log 日志去回滾。 事務 B 看到的場景,就是自己明明更新了,結(jié)果值卻沒了。
本質(zhì)就是事務 B 去修改了事務 A 修改過的值,但是此時事務 A 還沒提交,所以事務 A 隨時會回滾,導致事務 B 修改的值也沒了,這就是臟寫。

和 Java 中的 ABA 問題類似;但完全不一樣。就像一個隨時可以撕毀合同的客戶一樣??
臟讀
事務 A 更新了一行數(shù)據(jù)的值為 A 值,此時事務 B 去查詢了一下這行數(shù)據(jù)的 A 值 事務 B 此時拿到剛查出來的 A 值在做一些業(yè)務處理 事務 A 回滾了事務,導致剛才更新的 A 值沒了,此時那行數(shù)據(jù)的值回滾為 A 更新之前的舊值 事務 B 此時再次查詢那行數(shù)據(jù)的值,看到的居然此時是 A 更新之前的舊值
本質(zhì)其實就是事務 B 去查詢了事務 A 修改過的數(shù)據(jù),但是此時事務 A 還沒提交,所以事務 A 隨時會回滾導致事務 B 再次查詢就讀不到剛才事務 A 修改的數(shù)據(jù)了!這就是臟讀。

臟讀又稱無效數(shù)據(jù)的讀出,值得注意的是,臟讀一般是針對于 update 操作的。
無論是臟寫還是臟讀,都是因為一個事務去更新或者查詢了另外一個還沒提交的事務更新過的數(shù)據(jù)。因為另外一個事務還沒提交,所以他隨時可能會反悔會回滾,那么必然導致你更新的數(shù)據(jù)就沒了,或者你之前查詢到的數(shù)據(jù)就沒了,這就是臟寫和臟讀兩種坑爹場景。
不可重復讀
說到臟讀和和臟寫,面試官還可能問到“不可重復讀”。
假設我們有一個事務 A 開啟了,在這個事務 A 里會多次對一條數(shù)據(jù)進行查詢。
并且假設是事務 A 只能在事務 B 提交之后讀取到他修改的數(shù)據(jù)(避免臟讀)。
假設緩存頁里一條數(shù)據(jù)原來的值是 A 值,此時事務 A 開啟之后,第一次查詢這條數(shù)據(jù),讀取到的就是 ?A 值 事務 B 更新了那行數(shù)據(jù)的值為 B 值,同時事務 B 立馬提交了,然后事務 A 此時可是還沒提交! 事務 A 執(zhí)行期間第二次查詢數(shù)據(jù),此時查到的是事務 B 修改過的值,A 值,因為事務 B 已經(jīng)提交了,所以事務 A 可以讀到的

其實要說沒問題也可以是沒問題,畢竟事務 B 和事務 C 都提交之后,事務 A 多次查詢查到他們修改的值,是 ok 的。
但是你要說有問題,也可以是有問題的,就是事務 A 可能第一次查詢到的是 A 值,那么他可能希望的是在事務執(zhí)行期間,如果多次查詢數(shù)據(jù),都是同樣的一個 A 值,它希望這個 A 值是他重復讀取的時候一直可以讀到的!它希望這行數(shù)據(jù)的值是可重復讀的!
這個問題簡單來說,就是一個事務多次查詢一條數(shù)據(jù),結(jié)果每次讀到的值都不一樣,這個過程中可能別的事務會修改這條數(shù)據(jù)的值,而且修改值之后事務都提交了,結(jié)果導致人家每次查到的值都不一樣,都查到了提交事務修改過的值,這就是所謂的不可重復讀。
幻讀
接下來,還有一個幻讀,也是面試中必問的知識點。
事務 A,先發(fā)送一條 SQL 語句,他一開始查詢出來了 10 條數(shù)據(jù) 事務 B 往表里插入了幾條數(shù)據(jù),而且事務 B 還提交了,此時多了 2 行數(shù)據(jù)出來 事務 A 此時第二次查詢,還是那條 SQL,還是那個查詢條件,但是查詢出來的數(shù)據(jù)是 12 條
幻讀指的就是你一個事務用一樣的 SQL 多次查詢,結(jié)果每次查詢都會發(fā)現(xiàn)查到了一些之前沒看到過的數(shù)
據(jù)注意,幻讀特指的是你查詢到了之前查詢沒看到過的數(shù)據(jù)!此時就說你是幻讀了。

SQL標準四種事務隔離
這 4 種級別包括了:read uncommitted(讀未提交),read committed(讀已提交),repeatable read(可重復讀),serializable(串行化)。
| 事務級別 | 可能出現(xiàn)的問題 | 備注 |
|---|---|---|
| 讀未提交 | 臟讀,不可重復讀,幻讀 | |
| 讀已提交RC | 不可重復讀,幻讀 | |
| 可重復讀 RR | 幻讀 | |
| 串行化 | 性能降低 | 根本就不允許你多個事務并發(fā)執(zhí)行 |
MySQL事務隔離
但是要注意的一點是,MySQL 默認設置的事務隔離級別,是 RR 級別的(一般其他的數(shù)據(jù)庫是 RC)。并且 MySQL 的 RR 級別的語義跟 SQL 標準的 RR 級別不同的,畢竟 SQL 標準里規(guī)定 RR 級別是可以發(fā)生幻讀的,但是 MySQL 的 RR 級別就避免了幻讀。
MySQL 里執(zhí)行的事務,默認情況下不會發(fā)生臟寫、臟讀、不可重復讀和幻讀的問題,事務的執(zhí)行都是并行的,大家互相不會影響,我不會讀到你沒提交事務修改的值,即使你修改了值還提交了,我也不會讀到的,即使你插入了一行值還提交了,我也不會讀到的,總之,事務之間互相都完全不影響!
當然,要做到這么神奇和牛叉的效果,MySQL 是下了苦功夫的,后續(xù)我抽時間再給大家講解 MySQL 里的 ?MVCC 機制,就是多版本并發(fā)控制隔離機制,依托這個「MVCC」機制,就能讓 RR 級別避免不可重復讀和幻讀的問題。但是一般來說,真的其實不用修改這個級別,就用默認的 RR 其實就特別好,保證你每個事務跑的時候都沒人干擾,何樂而不為呢。
也可以參考我歷史文章中的 MVCC 部分的內(nèi)容。
undo log
undo log 日志結(jié)構(gòu)

一條日志必須得有自己的一個開始位置,這個沒什么好說的,很多存儲的數(shù)據(jù)都是這樣設計的 那么主鍵的各列長度和值是什么意思?大家都知道,你插入一條數(shù)據(jù),必然會有一個主鍵!如果你自己指定了一個主鍵,那么可能這個主鍵就是一個列,比如 id 之類的,也可能是多個列組成的一個主鍵,比如 id + name + age三個字段組成的一個聯(lián)合主鍵,也是有可能的。所以這個主鍵的各列長度和值,意思就是你插入的這條數(shù)據(jù)的主鍵的每個列,他的長度是多少,具體的值是多少。即使你沒有設置主鍵,MySQL 自己也會給你弄一個row_id作為隱藏字段,做你的主鍵(可見有主見是多么的重要??)。接著是表 id,這個就不用多說了,你插入一條數(shù)據(jù)必然是往一個表里插入數(shù)據(jù)的,那當然得有一個表id,記錄下來是往哪個表里插入的數(shù)據(jù)了。 undo log 日志編號,這個意思就是,每個 undo log 日志都是有自己的編號的。而在一個事務里會有多個 SQL 語句,就會有多個 undo log 日志,在每個事務里的 undo log 日志的編號都是從 0 開始的,然后依次遞增。 undo log 日志類型,就是 TRX_UNDO_INSERT_REC,insert 語句的 undo log 日志類型就是這個TRX_UNDO_INSERT_REC。undo log 日志的結(jié)束位置,這個自然也不用多說了,它就是告訴你 undo log 日志結(jié)束的位置是多少。
undo log 版本鏈
每條數(shù)據(jù)其實都有兩個隱藏字段,一個是trx_id,一個是roll_pointer,這個trx_id就是最近一次更新這條數(shù)據(jù)的事務 id,roll_pointer就是指向你了你更新這個事務之前生成的 undo log。
假設有一個事務 A(id=50),插入了一條數(shù)據(jù),那么此時這條數(shù)據(jù)的隱藏字段以及指向的 undo log 如下圖所示,插入的這條數(shù)據(jù)的值是值 A,因為事務 A 的 id 是 50,所以這條數(shù)據(jù)的 txr_id就是 50,roll_pointer指向一個空的 undo log,因為之前這條數(shù)據(jù)是沒有的。事務 B 跑來修改了一下這條數(shù)據(jù),把值改成了值 B,事務 B 的 id 是 58,那么此時更新之會生成一個 undo log 記錄之前的值,然后會讓 roll_pointer指向這個實際的 undo log 回滾日志,這個 undo log 就記錄你更新之前的那條數(shù)據(jù)的值。事務 C 又來修改了一下這個值為值 C,他的事務 id 是 69,此時會把數(shù)據(jù)行里的 txr_id改成 69,然后生成一條 undo log,記錄之前事務 B 修改的那個值。

「多個事務串行執(zhí)行的時候,每個人修改了一行數(shù)據(jù),都會更新隱藏字段txr_id和roll_pointer,同時之前多個數(shù)據(jù)快照對應的 undo log,會通過roll_pinter指針串聯(lián)起來,形成一個重要的版本鏈」!
ReadView
「生成readview時機」
RC 隔離級別:每次讀取數(shù)據(jù)前,都生成一個 readview;
RR 隔離級別:在第一次讀取數(shù)據(jù)前,生成一個 readview;
當我們執(zhí)行一個事務的時候,就給你生成一個 ReadView,里面比較關鍵的東西有 4 個。
「readview四個主要元素」
m_ids:表示在生成 readview 時,當前系統(tǒng)中活躍的讀寫事務 id 列表;min_trx_id:表示在生成 readview 時,當前系統(tǒng)中活躍的讀寫事務中最小的事務 id,也就是m_ids中最小的值;max_trx_id:表示生成 readview 時,系統(tǒng)中應該分配給下一個事務的 id 值;creator_trx_id:表示生成該 readview 的事務的事務 id;
「readview判斷版本鏈」
有了 readview,在訪問某條記錄時,按照以下步驟判斷記錄的某個版本是否可見。
如果被訪問版本的
trx_id,與 readview 中的creator_trx_id值相同,表明當前事務在訪問自己修改過的記錄,該版本可以被當前事務訪問;如果被訪問版本的
trx_id,小于 readview 中的min_trx_id值,表明生成該版本的事務在當前事務生成 readview 前已經(jīng)提交,該版本可以被當前事務訪問;如果被訪問版本的
trx_id,大于 readview 中的max_trx_id值,表明生成該版本的事務在當前事務生成 readview 后才開啟,該版本不可以被當前事務訪問;如果被訪問版本的
trx_id,值在 readview 的min_trx_id和max_trx_id之間,就需要判斷trx_id屬性值是不是在m_ids列表中?如果在:說明創(chuàng)建 readview 時生成該版本的事務還是活躍的,該版本不可以被訪問
如果不在:說明創(chuàng)建 readview 時生成該版本的事務已經(jīng)被提交,該版本可以被訪問;
通過 undo log 多版本鏈條,加上你開啟事務時候生產(chǎn)的一個 ReadView,然后再有一個查詢的時候,根據(jù) ReadView 進行判斷的機制,你就知道你應該讀取哪個版本的數(shù)據(jù)。而且他可以保證你只能讀到你事務開啟前,別的提交事務更新的值,還有就是你自己事務更新的值。假如說是你事務開啟之前,就有別的事務正在運行,然后你事務開啟之后 ,別的事務更新了值,你是絕對讀不到的!或者是你事務開啟之后,比你晚開啟的事務更新了值,你也是讀不到的!通過這套機制就可以實現(xiàn)多個事務并發(fā)執(zhí)行時候的數(shù)據(jù)隔離。
基于 ReadView 實現(xiàn) RR 事務
在 MySQL 中讓多個事務并發(fā)運行的時候能夠互相隔離,避免同時讀寫一條數(shù)據(jù)的時候有影響,是依托 undo log 版本鏈條和 ReadView 機制來實現(xiàn)的。
「第一步」:首先我們還是假設有一條數(shù)據(jù)是事務id=50的一個事務插入的,同時此時有事務 A 和事務 B 同時在運行,事務 A 的 id 是 60,事務 B 的 id 是 70。

「第二步」:這個時候,事務 A 發(fā)起了一個查詢,他就是第一次查詢就會生成一個 ReadView。

「第三步」:事務 A 基于這個 ReadView 去查這條數(shù)據(jù),通提供過對比這條數(shù)據(jù)的事務 id 和 ReadView 比較

「第四步」:事務B此時更新了這條數(shù)據(jù)的值為值 B,此時會修改trx_id為 70,同時生成一個 undo log 版本鏈,而且關鍵是事務 B 此時他還提交了,也就是說此時事務 B 已經(jīng)結(jié)束了,如下圖所示。

這個時候大家思考一個問題,ReadView 中的m_ids此時還會是 60 和 70 嗎?那必然是的,因為 ReadView 一旦生成了就不會改變了,這個時候雖然事務 B 已經(jīng)結(jié)束了,但是事務 A 的 ReadView 里,還是會有 60 和 70 兩個事務 id。
「第五步」:事務 A 繼續(xù)查詢數(shù)據(jù),結(jié)果發(fā)現(xiàn)這條數(shù)據(jù)的trx_id為7 0,如果被訪問版本的trx_id,值在 readview 的min_trx_id和max_trx_id之間,就需要判斷trx_id屬性值是不是在m_ids列表中,而我們發(fā)現(xiàn)我們的 ReadView 中,剛好有 70。說明創(chuàng)建 Readview 時生成該版本的事務 B 還是活躍的,該版本不可以被訪問。
因此這個時候只能順著指針往歷史版本鏈條上,找到下面一條數(shù)據(jù),trx_id為 50,是小于 ReadView 的min_trx_id的,說明在他開啟查詢之前,就已經(jīng)提交了這個事務了,所以事務 A 是可以查詢到這個值的,此時事務 A 查到的是原始值。

你事務 A 多次讀同一個數(shù)據(jù),每次讀到的都是一樣的值,除非是他自己修改了值,否則讀到的一直會一
樣的值。不管別的事務如何修改數(shù)據(jù),事務 A 的 ReadView 始終是不變的,他基于這個 ReadView 始終看到的值是一樣的!
RR事務解決幻讀
接著我們來看看幻讀的問題他是如何解決的。假設現(xiàn)在事務 A 先用select * from x where id>10來查詢,此時可能查到的就是 10 條數(shù)據(jù),而且讀到的是這條數(shù)據(jù)的原始值的那個版本。具體原因同上。
select?*?from?x?where?id>10
如果被訪問版本的trx_id,值在 readview 的min_trx_id和max_trx_id之間,就需要判斷trx_id屬性值是不是在m_ids列表中?
如果在:說明創(chuàng)建 readview 時生成該版本的事務還是活躍的,該版本不可以被訪問
如果不在:說明創(chuàng)建 readview 時生成該版本的事務已經(jīng)被提交,該版本可以被訪問;
「第一步」:現(xiàn)在有一個事務 C 插入了一條數(shù)據(jù)

「第二步」:此時事務 A 再次查詢,此時會發(fā)現(xiàn)符合條件的有 12 條數(shù)據(jù),10 條是原始值那個數(shù)據(jù),2 條是事務 C 插入的那條數(shù)據(jù),但是事務 C 插入的那條數(shù)據(jù)的trx_id是 80,這個 80 是大于自己的 ReadView 的max_trx_id的,說明是自己發(fā)起查詢之后,這個事務才啟動的,所以此時這條數(shù)據(jù)是不能查詢的。
因此事務 A 本次查詢,還是只能查到原始值 10 條數(shù)據(jù),如下圖。

所以大家可以看到,在這里,事務A根本不會發(fā)生幻讀,他根據(jù)條件范圍查詢的時候,每次讀到的數(shù)據(jù)
都是一樣的,不會讀到人家插入進去的數(shù)據(jù),這都是依托 ReadView 機制實現(xiàn)的。
MVCC機制
multi-version concurrent control簡稱 MVCC,就是多版本并發(fā)控制機制。
MySQL 實現(xiàn) MVCC 機制的時候,是基于 undo log 多版本鏈條 + ReadView 機制來做的,默認的 RR 隔離級別,就是基于這套機制來實現(xiàn)的,依托這套機制實現(xiàn)了 RR 級別,除了避免臟寫、臟讀、不可重復讀,還能避免幻讀問題。因此一般來說我們都用默認的 RR 隔離級別就好了。
這就使得別的事務可以修改這條記錄,反正每次修改都會在版本鏈中記錄。SELECT 可以去版本鏈中拿記錄,這就實現(xiàn)了讀寫,寫讀的并發(fā)執(zhí)行,提升了系統(tǒng)的性能。
往期內(nèi)容閱讀:MySQL當中的 “My” 是什么意思?
