MySQL 8.0 MVCC 核心源碼解析
并發(fā)事務(wù)帶來的問題(現(xiàn)象)
臟讀:一個事務(wù)讀取到另一個事務(wù)更新但還未提交的數(shù)據(jù),如果另一個事務(wù)出現(xiàn)回滾或者進一步更新,則會出現(xiàn)問題。

不可重復(fù)讀:在一個事務(wù)中兩次次讀取同一個數(shù)據(jù)時,由于在兩次讀取之間,另一個事務(wù)修改了該數(shù)據(jù),所以出現(xiàn)兩次讀取的結(jié)果不一致。


核心數(shù)據(jù)結(jié)構(gòu)
trx_sys_t:事務(wù)系統(tǒng)中央存儲器數(shù)據(jù)結(jié)構(gòu)
struct trx_sys_t {TrxSysMutex mutex; /*! 互斥鎖 */MVCC *mvcc; /*! mvcc */volatile trx_id_t max_trx_id; /*! 要分配給下一個事務(wù)的事務(wù)id*/std::atomic<trx_id_t> min_active_id; /*! 最小的活躍事務(wù)Id */// 省略...trx_id_t rw_max_trx_id; /*!< 最大讀寫事務(wù)Id */// 省略...trx_ids_t rw_trx_ids; /*! 當(dāng)前活躍的讀寫事務(wù)Id列表 */Rsegs rsegs; /*!< 回滾段 */// 省略...};
MVCC:MVCC 讀取視圖管理器
class MVCC {public:// 省略.../** 創(chuàng)建一個視圖 */void view_open(ReadView *&view, trx_t *trx);/** 關(guān)閉一個視圖 */void view_close(ReadView *&view, bool own_mutex);/** 釋放一個視圖 */void view_release(ReadView *&view);// 省略.../** 判斷視圖是否處于活動和有效狀態(tài) */static bool is_view_active(ReadView *view) {ut_a(view != reinterpret_cast(0x1)); return (view != NULL && !(intptr_t(view) & 0x1));}// 省略...private:typedef UT_LIST_BASE_NODE_T(ReadView) view_list_t;/** 空閑可以被重用的視圖*/view_list_t m_free;/** 活躍或者已經(jīng)關(guān)閉的 Read View 的鏈表 */view_list_t m_views;};
ReadView:視圖,某一時刻的一個事務(wù)快照
class ReadView {// 省略...private:/** 高水位,大于等于這個ID的事務(wù)均不可見*/trx_id_t m_low_limit_id;/** 低水位:小于這個ID的事務(wù)均可見 */trx_id_t m_up_limit_id;/** 創(chuàng)建該 Read View 的事務(wù)ID*/trx_id_t m_creator_trx_id;/** 創(chuàng)建視圖時的活躍事務(wù)id列表*/ids_t m_ids;/** 配合purge,標(biāo)識該視圖不需要小于m_low_limit_no的UNDO LOG,* 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 標(biāo)記視圖是否被關(guān)閉*/bool m_closed;// 省略...};
增加隱藏字段
為了實現(xiàn) MVCC,InnoDB 會向數(shù)據(jù)庫中的每行記錄增加三個字段:

源碼分析
在源碼中,添加這3個字段的方法在:/storage/innobase/dict/dict0dict.cc 的?dict_table_add_system_columns 方法中,核心部分如下圖。

增刪改的底層操作
當(dāng)我們更新一條數(shù)據(jù),InnoDB 會進行如下操作:
加鎖:對要更新的行記錄加排他鎖
寫 undo log:將更新前的記錄寫入 undo log,并構(gòu)建指向該 undo log 的回滾指針 roll_ptr
更新行記錄:更新行記錄的 DB_TRX_ID 屬性為當(dāng)前的事務(wù)Id,更新 DB_ROLL_PTR 屬性為步驟2生成的回滾指針,將此次要更新的屬性列更新為目標(biāo)值
寫 redo log:DB_ROLL_PTR 使用步驟2生成的回滾指針,DB_TRX_ID 使用當(dāng)前的事務(wù)Id,并填充更新后的屬性值
處理結(jié)束,釋放排他鎖
刪除操作:在底層實現(xiàn)中是使用更新來實現(xiàn)的,邏輯基本和更新操作一樣,幾個需要注意的點:1)寫 undo log 中,會通過 type_cmpl 來標(biāo)識是刪除還是更新,并且不記錄列的舊值;2)這邊不會直接刪除,只會給行記錄的 info_bits 打上刪除標(biāo)識(REC_INFO_DELETED_FLAG),之后會由專門的 purge 線程來執(zhí)行真正的刪除操作。
插入操作:相比于更新操作比較簡單,就是新增一條記錄,DB_TRX_ID?使用當(dāng)前的事務(wù)Id,同樣會有 undo log 和 redo log。
源碼分析
更新行記錄的核心源碼在:/storage/innobase/btr/btr0cur.cc/btr_cur_update_in_place 方法,核心部分如下圖。

構(gòu)建一致性讀取視圖(ReadView)
m_ids:創(chuàng)建 ReadView 時當(dāng)前系統(tǒng)中活躍的事務(wù) Id 列表,可以理解為生成 ReadView 那一刻還未執(zhí)行提交的事務(wù),并且該列表是個升序列表。 m_up_limit_id:低水位,取 m_ids 列表的第一個節(jié)點,因為 m_ids 是升序列表,因此也就是 m_ids 中事務(wù) Id 最小的那個。 m_low_limit_id:高水位,生成 ReadView 時系統(tǒng)將要分配給下一個事務(wù)的 Id 值。 m_creator_trx_id:創(chuàng)建該 ReadView 的事務(wù)的事務(wù) Id。
row_search_mvcc ->?trx_assign_read_view ->?MVCC::view_open ->?
ReadView::prepare,源碼如下:

最后,會將這個創(chuàng)建的 ReadView 添加到 MVCC 的 m_views 中。
視圖可見性判斷:SQL 查詢走聚簇索引
如果被訪問版本的 trx_id 與 ReadView 中的 m_creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
如果被訪問版本的 trx_id 小于 ReadView 中的 m_up_limit_id(低水位),表明被訪問版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
如果被訪問版本的 trx_id 大于等于 ReadView 中的 m_low_limit_id(高水位),表明被訪問版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
如果被訪問版本的 trx_id 屬性值在 ReadView 的 m_up_limit_id 和 m_low_limit_id 之間,那就需要判斷 trx_id 屬性值是不是在 m_ids 列表中,這邊會通過二分法查找。如果在,說明創(chuàng)建 ReadView 時生成該版本的事務(wù)還是活躍的,該版本不可以被訪問;如果不在,說明創(chuàng)建 ReadView 時生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問。
源碼分析
走聚簇索引的核心流程在?row_search_mvcc?方法,如下:

視圖可見性判斷在方法:changes_visible,調(diào)用鏈如下:
row_search_mvcc ->?lock_clust_rec_cons_read_sees?->?
changes_visible,源碼如下:

判斷記錄是否被打上 delete_flag 標(biāo)的方法在:/storage/innobase/include/rem0rec.ic?的?rec_get_deleted_flag?方法中,如下圖。

獲取記錄的上一個版本
獲取記錄的上一個版本,主要是通過 DB_ROLL_PTR 來實現(xiàn),核心流程如下:
獲取記錄的回滾指針?DB_ROLL_PTR、獲取記錄的事務(wù)Id?
通過回滾指針拿到對應(yīng)的 undo log
解析 undo log,并使用 undo log 構(gòu)建用于更新向量 UPDATE
構(gòu)建記錄的上一個版本:先用記錄的當(dāng)前版本填充,然后使用 UPDATE(undo log)進行覆蓋。
源碼解析
構(gòu)建記錄的上一個版本:trx_undo_prev_version_build,調(diào)用鏈如下:
row_search_mvcc ->?row_sel_build_prev_vers_for_mysql?->?row_vers_build_for_consistent_read ->?trx_undo_prev_version_build,源碼如下:

視圖可見性判斷:SQL 查詢走普通(二級)索引
BATJTMD 面試必問的 MySQL,你懂了嗎? 只分析了走聚簇索引的情況,本文簡單的介紹下走普通(二級)索引的情況。
當(dāng)走普通索引時,判斷邏輯如下:
判斷被訪問索引記錄所在頁的最大事務(wù) Id 是否小于?ReadView 中的?m_up_limit_id(低水位),如果是則代表該頁的最后一次修改事務(wù) Id 在 ReadView?創(chuàng)建前以前已經(jīng)提交,則必然可以訪問;如果不是,并不代表一定不可以訪問,道理跟走聚簇索引一樣,事務(wù) Id 大的也可能提交比較早,所以需要做進一步判斷,見步驟2。
使用 ICP(Index Condition Pushdown)根據(jù)索引信息來判斷搜索條件是否滿足,這邊主要是在使用聚簇索引判斷前先進行過濾,這邊有三種情況:a)ICP 判斷不滿足條件但沒有超出掃描范圍,則獲取下一條記錄繼續(xù)查找;b)如果不滿足條件并且超出掃描返回,則返回?DB_RECORD_NOT_FOUND;c)如果?ICP 判斷符合條件,則會獲取對應(yīng)的聚簇索引來進行可見性判斷。
源碼分析
普通(非聚簇)索引的視圖可見性判斷在方法:lock_sec_rec_cons_read_sees,調(diào)用鏈如下:
row_search_mvcc ->?lock_sec_rec_cons_read_sees,源碼如下:


擴展理解
ICP(Index Condition Pushdown)
ICP 是 MySQL 5.6 引入的一個優(yōu)化,根據(jù)官方的說法:ICP 可以減少存儲引擎訪問基表的次數(shù) 和 MySQL 訪問存儲引擎的次數(shù),這邊涉及到 MySQL 底層的處理邏輯,不是本文重點,這邊不進行細(xì)講。
這邊用官方的例子簡單介紹下,我們有張 people 表,索引定義為:INDEX (zipcode, lastname, firstname),對于以下這個 SQL:
SELECT * FROM peopleWHERE zipcode='95054'AND lastname LIKE '%etrunia%'AND address LIKE '%Main Street%';
當(dāng)沒有使用 ICP 時:此查詢會使用該索引,但是必須掃描 people 表所有符合 zipcode='95054' 條件的記錄。
當(dāng)使用 ICP 時:不僅會使用?zipcode 的條件來進行過濾,還會使用?(lastname LIKE '%etrunia%')來進行過濾,這樣可以避免掃描符合 zipcode 條件而不符合 lastname 條件匹配的記錄行?。
ICP 的官方文檔:https://dev.mysql.com/doc/refman/8.0/en/index-condition-pushdown-optimization.html
當(dāng)前讀和快照讀
當(dāng)前讀:官方叫做 Locking Reads(鎖定讀?。?,讀取數(shù)據(jù)的最新版本。常見的 update/insert/delete、還有 select ... for update、select ... lock in share mode 都是當(dāng)前讀。
官方文檔:https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
快照讀:官方叫做?Consistent Nonlocking Reads(一致性非鎖定讀取,也叫一致性讀?。x取快照版本,也就是 MVCC 生成的 ReadView。用于普通的 select 的語句。
官方文檔:https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
MVCC 解決了幻讀了沒有?
幾個例子
例子1:RR(RC) 真正生成 ReadView 的時機

解析:RR 生成 ReadView 的時機是事務(wù)第一個 select 的時候,而不是事務(wù)開始的時候。右邊的例子中,事務(wù)1在事務(wù)2提交了修改后才執(zhí)行第一個 select,因此生成的 ReadView 中,a 的是 100 而不是事務(wù)1剛開始時的 50。
例子2:RR 和 RC 生成 ReadView 的區(qū)別

解析:RR 級別只在事務(wù)第一次 select 時生成一次,之后一直使用該 ReadView。而 RC 級別則在每次 select 時,都會生成一個 ReadView,所以 在第二次 select 時,讀取到了事務(wù)2對于 a 的修改值。
最后
MySQL 的源碼主要是?c++ 寫的,因此自己看起來比較吃力,花了挺多時間學(xué)習(xí)整理的。如果你能掌握本文的內(nèi)容,面試?Java 崗位,無論是哪個公司,相信都能讓面試官眼前一亮。
現(xiàn)在互聯(lián)網(wǎng)的競爭越來越激烈,如果很多東西都只停留在表面,很難取得面試官的“芳心”,只有在適當(dāng)?shù)臅r候亮出自己的“長劍”,才能在眾多候選人中凸顯出自己的與眾不同。你需要向面試官證明,為什么是你而不是其他人。
