作者:京東零售 李澤陽
最近在閱讀《認知覺醒》這本書,里面有句話非常打動我:通過自己的語言,用最簡單的話把一件事情講清楚,最好讓外行人也能聽懂,
也許這就是大道至簡,只是我們習慣了煩瑣和復雜,
希望借助今天這篇文章,能用大白話說清楚這個相對比較底層和復雜的MVCC機制,
在開始之前,先拋出一個問題:我們都知道,目前(MySQL 5.6以上)資料庫已普遍使用InnoDB存盤引擎,InnoDB相對于MyISAM存盤引擎其中一個好處就是在資料庫級別鎖和表級別鎖的基礎上支持了行鎖,還有就是支持事務,保證一組資料庫操作要么成功,要么失敗,基于此,問題來了,在InnoDB默認隔離級別(可重復讀)下,一個事務想要更新一行資料,如果剛好有另外一個事務擁有這個行鎖,那么這個事務就會進入等待狀態,既然進入等待狀態,那么等到這個事務獲取到行鎖要更新資料的時候,它讀取到的值是什么呢?
具體的問題見下圖,我們設定有一張表user,初始化陳述句如下,試想在這樣的場景下,事務A三次查詢的值分別是什么?
create table `user` (
`id` bigint not null,
`name` varchar(50) default null,
PROMARY KEY (`id`)
) ENGINE = InnoDB;
insert into user(id,name) values (1,'A');

想要把這件事情回答正確,我們先來鋪墊一下基礎知識,
提到事務,首先會想到的就是ACID(Atomic原子性、Consist一致性、Isolate隔離性、Durable持久性),今天我們主要關注隔離性,當有多個事務同時執行發生并發時,資料庫可能會出現臟讀、不可重復讀和幻讀等問題,為了解決這些問題,“隔離級別”這位大哥上場,包含:讀未提交、讀已提交、可重復讀和串行,
但我們都知道,隔離級別越高,執行效率越低,畢竟大哥就是大哥,級別越高,越謹慎,常在河邊走哪能不濕鞋,
我們通過一個例子簡單說一下這四種隔離級別:

? 讀未提交:一個事務還未提交,它的變更就能被其他事務看到,V1為B,V2為B,V3為B,
? 讀已提交:一個事務提交之后,變更結果對其他事務可見,V1為A,V2和V3為B,
? 可重復讀:一個事務執行程序中看到的資料與事務啟動時一致,V1為A,V2為A,V3為B,
? 串行:不管讀和寫,加鎖就完了,就是干!V1和V2均為A,V3為B,
事務是怎么實作的呢?實際上,事務執行時,資料庫會創建一個視圖,讀未提交直接回傳最新值,沒有視圖概念;串行是直接加鎖避免并發訪問;讀已提交是在每個SQL陳述句開始執行時創建的視圖,可重復讀的視圖是在事務啟動的時候創建的,整個事務都會使用這個視圖,這樣的話,上面四種不同隔離級別下的V1、V2、V3值便對號入座,有了結果,
MySQL是怎么實作的呢?我們以MySQL默認的可重復讀隔離級別為例,實際上每條行記錄在更新時都會記錄一潭訓滾日志,也就是大家常說的undo log,通過回滾操作,都可以得到前一個狀態的值,假設name值從初始值A被依次更新為B、C、D,我們看一下回滾日志:

當前值是D,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的視圖,看到的值也就不一樣,在視圖1、2、3、4里面,記錄的name值分別是A、B、C、D,同一條行記錄在資料庫中可以存在多個版本,這就是多版本并發控制(MVCC),對于視圖1,如果想要將name值回到A,那么就要依次執行圖中所有回滾操作,
到這里,你已經接觸到了MVCC的概念,也許你已經對文章最開始的問題有了一點點想法,別著急,我們先來簡單總結下MVCC的特點:
MVCC的出現使得一條行記錄在不同隔離級別下不同的事務操作會形成一條不同版本的鏈路,從而實作在不加鎖的前提下使不同事務的讀寫操作能夠并發安全執行,這個版本鏈就是通過回滾日志undo log實作的,用大白話說,你這個事務想要查詢一條行記錄,MVCC會通過你這個事務所在視圖確認版本鏈中哪個版本的行資料對你可見,剛才我們提到,四種隔離級別下,只有讀已提交和可重復讀會用到視圖,對于讀已提交,MVCC會在每次查詢前都會生成一個視圖,可重復讀隔離級別只會在第一次查詢時生成一個視圖,之后在這個事務中的所有查詢操作都會重復使用這個視圖,行業上,將創建視圖的那一刻稱為快照,晃你一下子,讓你激靈激靈,別發生臟讀,變臟嘍~
想要解決文章最開始的那個問題,我們還得展開說說版本鏈是如何形成的和快照的原理,稍有枯燥,先忍一下,耐心看下去,乖~
對于InnoDB存盤引擎來說,主鍵索引(也稱為聚簇索引)記錄中除了正常的欄位資料外,還包含兩個隱藏列:

(1)trx_id:每次一個事務想要對主鍵索引進行更新、洗掉和新增時,都會把這個事務的事務id賦值給trx_id欄位,注意事務id嚴格遞增,且查詢操作不會分配事務id,即trx_id = 0;
(2)roll_point:每次一個事務對主鍵索引進行更新時,都會把舊的版本寫入到undo日志中,roll_point相當于一個指標,通過它可以找到這條記錄修改前的資訊,
我們以可重復讀隔離級別為例,為了尚未提交的更新結果對其他事務不可見,InnoDB在創建視圖時,有以下四部分組成:
? m_ids:表示生成視圖時,當前系統中“活躍”的讀寫事務的事務id串列,這里的活躍大白話就是事務尚未提交;
? min_trx_id:表示在生成視圖時,當前系統中活躍的讀寫事務中最小的事務id,即m_ids中的最小值;
? max_trx_id:表示生成視圖時系統應該分配給下一個事務的id值;
? creator_trx_id:表示生成該視圖的事務id,
概念比較多,舉個例子,現在有事務id分別是1、2、3三個事務,1和2事務尚未提交,3事務已提交,這個時候如果來了一個新事務,那么它創建的視圖對應這幾個引數分別為:m_ids包含1、2,min_trx_id為1,max_trx_id為4,
關鍵的知識點來了,如何根據某個事務生成的視圖,判斷版本鏈上的某個版本對這個事務可見呢?
遵循下面步驟:
1、版本鏈上的不同版本trx_id值如果與這個視圖的creator_trx_id值相同,說明當前事務在訪問它自己修改過的記錄,所以被訪問的版本對當前事務可見,一家人還是認識一家人的~
2、版本鏈上的不同版本trx_id值小于這個視圖的min_trx_id值,說明這個版本的事務在當前事務生成視圖之前就已經提交了,所以被訪問的版本對當前事務可見,
3、版本鏈上的不同版本的trx_id值大于或等于這個視圖的max_trx_id值,說明這個版本的事務在當前事務之后才開啟,所以被訪問版本對當前事務不可見,
4、版本鏈上的不同版本的trx_id值在這個視圖的min_trx_id和max_trx_id之間,需要進一步判斷被訪問版本trx_id值是不是在m_ids中,如果在,說明當前事務是活躍的,被訪問版本對當前事務不可見,如果不在,說明被訪問版本的事務已經提交了,被訪問版本對當前事務可見,
比較繞是不是,千萬別暈,兄弟呀~,大白話解釋一下,設定某個事務生成的視圖瞬間(也就是快照),這個事務的id為creator_trx_id,那么有下面三種可能:

1、如果creator_trx_id落在綠色部分,表示被訪問的版本是已提交的事務或者就是當前事務自己生成的,這個資料是可見的;
2、如果creator_trx_id落在紅色部分,表示被訪問的版本還未開啟,資料不可見;
3、如果creator_trx_id落在黃色部分,包括兩種情況:
若creator_trx_id在m_ids集合中,表示被訪問的版本尚未提交,資料不可見;
若creator_trx_id不在m_ids集合中,表示被訪問的版本已經已經提交了,資料可見,
知道了這個之后,我們就可以回答文章最開始那個問題了,在隔離級別為可重復讀的情況下(這里的隱含條件就是可重復讀隔離級別只會在第一次查詢時生成一個視圖,之后在這個事務中的所有查詢操作都會重復使用這個視圖)分析一波:
以文章開頭的例子,設定事務B的事務id=100,事務C的事務id=200,當事務B尚未提交時,id=1這條記錄的版本鏈是這樣的:

這個時候我們看一下事務A第一個select陳述句,注意查詢操作的事務trx_id=0,在執行select陳述句時會創建一個視圖,這個視圖的m_ids={100},min_trx_id=100,max_trx_id=101,creator_trx_id=0,
然后在版本鏈中挑選可見的資料記錄,從圖中可以看到最新版本的name值是B,最新版本的trx_id值為100,在m_ids集合中,這個版本資料不可見,根據roll_point跳到下一個版本;
下一個版本的name值是A,這個版本的trx_id=99,小于min_trx_id,這個版本資料是可見的,所以回傳name為A的記錄,即V1為A,
我們繼續,事務B這時進行了commit提交,此時事務C已經開啟,那么事務A第二個select陳述句不會創建一個新的視圖,而是重新利用第一次創建的視圖,最新版本的trx_id為100,在m_ids中,資料不可見,即V2=A;
接下來,事務C進行了更新操作,此時版本鏈發生的改變如下:

事務C接著進行了commit提交,此時事務A第三次select陳述句也不會創建一個新的視圖,最新版本的trx_id為200,大于max_trx_id,資料不可見,即V3=A,
到這里,MVCC就結束啦,留一個小問題,如果是讀已提交隔離級別,那么文章開頭的例子中V1、V2、V3的值又分別是什么呢?答案在最后哦,
最后,我們再來總結一下MVCC的作用,使用可重復讀隔離級別的事務在查詢時,僅會使用第一次select時生成的視圖,相比于讀已提交隔離級別每次查詢都會生成一個新的視圖,可重復讀在查詢時使用的視圖版本不會那么新,因此有些已經提交的事務對行記錄進行修改時對查詢事務就不可見,進而避免了不可重復讀現象的發生,同時也避免了臟讀,
小問題答案:
讀已提交隔離級別下,每次select查詢都會生成一個新的視圖,基于此,分析如下:
事務A第一個select陳述句,注意查詢操作的事務trx_id=0,在執行select陳述句時會創建一個視圖,這個視圖的m_ids={100},min_trx_id=100,max_trx_id=101,creator_trx_id=0,
然后在版本鏈中挑選可見的資料記錄,從圖中可以看到最新版本的name值時B,最新版本的trx_id值為100,在m_ids集合中,這個版本資料不可見,根據roll_point跳到下一個版本;
下一個版本的name值是A,這個版本的trx_id=99,小于min_trx_id,這個版本資料是可見的,所以回傳name為A的記錄,即V1為A,
事務B這時進行了commit提交,此時事務C已經開啟,那么事務A第二個select陳述句會創建一個新的視圖,這個視圖的m_ids={200},min_trx_id=200,max_trx_id=201,creator_trx_id=0,版本鏈沒有發生變化,最新版本trx_id值為100,小于min_trx_id,資料可見,即V2=B;
事務C接著進行了commit提交,此時事務A第三次select陳述句會創建一個新的視圖,這個視圖的m_ids={},min_trx_id不存在,max_trx_id=201,creator_trx_id=0,在版本鏈中挑選可見的資料記錄,從圖中可以看到最新版本的name值為C,最新版本的trx_id值為200,小于max_trx_id且不在m_ids中,則資料可見,即V3=C,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/549793.html
標籤:MySQL
