MySQL多版本併發控制機制(MVCC)-源碼淺析

<h1>MySQL多版本併發控制機制(MVCC)-源碼淺析</h1> <h1>前言</h1> <p>做爲一個數據庫愛好者,本身動手寫過簡單的SQL解析器以及存儲引擎,但感受仍是不夠過癮。&lt;&lt;事務處理-概念與技術&gt;&gt;誠然講的很是透徹,但只能提綱挈領,不能讓你玩轉某個真正的數據庫。感謝cmake,可以讓我在mac上用xcode去debug MySQL,從而能去領略它的各類實現細節。<br /> 筆者一直對數據庫的隔離性很好奇,此篇博客就是我debug MySQL過程當中的偶有所得。<br /> (注:本文的MySQL採用的是MySQL-5.6.35版本)</p> <h1>MVCC(多版本併發控制機制)</h1> <p>隔離性也能夠被稱做併發控制、可串行化等。談到併發控制首先想到的就是鎖,MySQL經過使用兩階段鎖的方式實現了更新的可串行化,同時爲了加速查詢性能,採用了MVCC(Multi Version Concurrency Control)的機制,使得不用鎖也能夠獲取一致性的版本。</p> <h2>Repeatable Read</h2> <p>MySQL的經過MVCC以及(Next-Key Lock)實現了可重複讀(Repeatable Read),其思想(MVCC)就是記錄數據的版本變遷,經過精巧的選擇不一樣數據的版本從而可以對用戶呈現一致的結果。以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/ee11025c7a0d8f069b511048a0d3157b2b4.jpg" alt="" /><br /> 上圖中,(A=50|B=50)的初始版本爲1。<br /> 1.事務t1在select A時候看到的版本爲1,即A=50<br /> 2.事務t2對A和B的修改將版本升級爲2,即A=0,B=100<br /> 3.事務t1再此select B的時候看到的版本仍是1, 即B=50<br /> 這樣就隔離了版本的影響,A+B始終爲100。</p> <h2>Read Commit</h2> <p>而若是不經過版本控制機制,而是讀到最近提交的結果的話,則隔離級別是read commit,以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/e75c7db99782bdb353a3e1847a48e4bde02.jpg" alt="" /><br /> 在這種狀況下,就須要使用鎖機制(例如select for update)將此A,B記錄鎖住,從而得到正確的一致結果,以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/29d0b7e6eccdffe20a62b1fb9d2b0fbc1c2.jpg" alt="" /></p> <h2>MVCC的優點</h2> <p>當咱們要對一些數據作一些只讀操做來檢查一致性,例如檢查帳務是否對齊的操做時候,並不但願加上對性能損耗很大的鎖。這時候MVCC的一致性版本就有很大的優點了。</p> <h1>MVCC(實現機制)</h1> <p>本節就開始談談MVCC的實現機制,注意MVCC僅僅在純select時有效(不包括select for update,lock in share mode等加鎖操做,以及update\insert等)。</p> <h2>select運行棧</h2> <p>首先咱們追蹤一下一條普通的查詢sql在mysql源碼中的運行過程,sql爲(select * from test); <img src="https://oscimg.oschina.net/oscnet/81f6d6cc07e2424d4182557c011f84baf1c.jpg" alt="" /><br /> 其運行棧爲:</p>mysql

handle_one_connection  MySQL的網絡模型是one request one thread
 |-do_handle_one_connection
	|-do_command
		|-dispatch_command
			|-mysql_parse	解析SQL
				|-mysql_execute_command
					|-execute_sqlcom_select	執行select語句
						|-handle_select
							...一堆parse join 等的操做,當前並不關心
							|-*tab-&gt;read_record.read_record 讀取記錄

<p>因爲mysql默認隔離級別是repeatable_read(RR),因此read_record重載爲 rr_sequential(當前咱們並不關心select經過index掃描出row以後再經過condition過濾的過程)。繼續追蹤:</p>sql

read_record
 |-rr_sequential
	|-ha_rnd_next
		|-ha_innobase::rnd_next 這邊就已經到了innodb引擎了
			|-general_fetch
				|-row_search_for_mysql
					|-lock_clust_rec_cons_read_sees 這邊就是判斷並選擇版本的地方

<p>讓咱們看下該函數內部:</p>數據庫

bool lock_clust_rec_cons_read_sees(const rec_t* rec /*由innodb掃描出來的一行*/,....){
	...
	// 從當前掃描的行中獲取其最後修改的版本trx_id(事務id)
	trx_id = row_get_rec_trx_id(rec, index, offsets);
	// 經過參數(一致性快照視圖和事務id)決定看到的行快照
	return(read_view_sees_trx_id(view, trx_id));
}

<h2>read_view的建立過程</h2> <p>咱們先關注一致性視圖的建立過程,咱們先看下read_view結構:</p>數組

struct read_view_t{
	// 因爲是逆序排列,因此low/up有所顛倒
	// 能看到當前行版本的高水位標識,&gt; low_limit_id皆不能看見
	trx_id_t	low_limit_id;
	// 能看到當前行版本的低水位標識,&lt; up_limit_id皆能看見
	trx_id_t	up_limit_id;
	// 當前活躍事務(即未提交的事務)的數量
	ulint		n_trx_ids;
	// 以逆序排列的當前獲取活躍事務id的數組
	// 其up_limit_id&lt;tx_id&lt;low_limit_id
	trx_id_t*	trx_ids;	
	// 建立當前視圖的事務id
	trx_id_t	creator_trx_id;
	// 事務系統中的一致性視圖鏈表
	UT_LIST_NODE_T(read_view_t) view_list;
};

<p>而後經過debug,發現建立read_view結構也是在上述的rr_sequential中操做的,繼續跟蹤調用棧:</p>xcode

rr_sequential
 |-ha_rnd_next
 	|-rnd_next
 		|-index_first 在start_of_scan爲true時候走當前分支index_first
 			|-index_read
 				|-row_search_for_mysql
 					|-trx_assign_read_view

<p>咱們看下row_search_for_mysql裏的一個分支:</p>網絡

row_search_for_mysql:
// 這邊只有select不加鎖模式的時候纔會建立一致性視圖
else if (prebuilt-&gt;select_lock_type == LOCK_NONE) {		// 建立一致性視圖
		trx_assign_read_view(trx);
		prebuilt-&gt;sql_stat_start = FALSE;
}

<p>上面的註釋就是select for update(in share model)不會走MVCC的緣由。讓咱們進一步分析trx_assign_read_view函數:</p>併發

trx_assign_read_view
 |-read_view_open_now
 	|-read_view_open_now_low

<p>好了,終於到了建立read_view的主要階段,主要過程以下圖所示:</p> <p><img src="https://oscimg.oschina.net/oscnet/31fc0a3c8751d4d92d84e6bccc691c4d150.jpg" alt="" /></p> <p>代碼過程爲:</p>函數

static read_view_t* read_view_open_now_low(trx_id_t	cr_trx_id,mem_heap_t*	heap)
{
	read_view_t*	view;
	// 當前事務系統中最大的事務id設置爲low_limit_no
	view-&gt;low_limit_no = trx_sys-&gt;max_trx_id;
	view-&gt;low_limit_id = view-&gt;low_limit_no;
	// CreateView構造函數,會將非當前事務和已經在內存中提交的事務給剔除,即判斷條件爲
	// trx-&gt;id != m_view-&gt;creator_trx_id&amp;&amp; !trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)的
	// 才加入當前視圖列表
	ut_list_map(trx_sys-&gt;rw_trx_list, &amp;trx_t::trx_list, CreateView(view));
	if (view-&gt;n_trx_ids &gt; 0) {
		// 將當前事務系統中的最小id設置爲up_limit_id,由於是逆序排列
		view-&gt;up_limit_id = view-&gt;trx_ids[view-&gt;n_trx_ids - 1];
	} else {
		// 若是當前沒有非當前事務以外的活躍事務,則設置爲low_limit_id
		view-&gt;up_limit_id = view-&gt;low_limit_id;
	}
	// 忽略purge事務,purge時,當前事務id是0
	if (cr_trx_id &gt; 0) {
		read_view_add(view);
	}
	// 返回一致性視圖
	return(view);
}

<h2>行版本可見性:</h2> <p>由上面的lock_clust_rec_cons_read_sees可知,行版本可見性由read_view_sees_trx_id函數判斷:</p>性能

/*********************************************************************//**
Checks if a read view sees the specified transaction.
@return	true if sees */
UNIV_INLINE
bool
read_view_sees_trx_id(
/*==================*/
	const read_view_t*	view,	/*!&lt; in: read view */
	trx_id_t		trx_id)	/*!&lt; in: trx id */
{
	if (trx_id &lt; view-&gt;up_limit_id) {

		return(true);
	} else if (trx_id &gt;= view-&gt;low_limit_id) {

		return(false);
	} else {
		ulint	lower = 0;
		ulint	upper = view-&gt;n_trx_ids - 1;

		ut_a(view-&gt;n_trx_ids &gt; 0);

		do {
			ulint		mid	= (lower + upper) &gt;&gt; 1;
			trx_id_t	mid_id	= view-&gt;trx_ids[mid];

			if (mid_id == trx_id) {
				return(FALSE);
			} else if (mid_id &lt; trx_id) {
				if (mid &gt; 0) {
					upper = mid - 1;
				} else {
					break;
				}
			} else {
				lower = mid + 1;
			}
		} while (lower &lt;= upper);
	}

	return(true);
}

<p>其實上述函數就是一個二分法,read_view其實保存的是當前活躍事務的全部事務id,若是當前行版本對應修改的事務id不在當前活躍事務裏面的話,就返回true,表示當前版本可見,不然就是不可見,以下圖所示。</p> <p><img src="https://oscimg.oschina.net/oscnet/ca65123f9602c9a74c5b46cdfa6f8b0050e.jpg" alt="" /><br /> 接上述lock_clust_rec_cons_read_sees的返回:</p>fetch

if (UNIV_LIKELY(srv_force_recovery &lt; 5)
			    &amp;&amp; !lock_clust_rec_cons_read_sees(
				    rec, index, offsets, trx-&gt;read_view)){
	// 當前處理的是當前版本不可見的狀況
	// 經過undolog來返回到一致的可見版本
	err = row_sel_build_prev_vers_for_mysql(
					trx-&gt;read_view, clust_index,
					prebuilt, rec, &amp;offsets, &amp;heap,
					&amp;old_vers, &amp;mtr);			    
} else{
	// 可見,而後返回
}

<h2>undolog搜索可見版本的過程</h2> <p>咱們如今考察一下row_sel_build_prev_vers_for_mysql函數:</p>

row_sel_build_prev_vers_for_mysql
 |-row_vers_build_for_consistent_read

<p>主要是調用了row_ver_build_for_consistent_read方法返回可見版本:</p>

dberr_t row_vers_build_for_consistent_read(...)
{
	......
	for(;;){
		err = trx_undo_prev_version_build(rec, mtr,version,index,*offsets, heap,&amp;prev_version);
		......
		trx_id = row_get_rec_trx_id(prev_version, index, *offsets);
		// 若是當前row版本符合一致性視圖,則返回
		if (read_view_sees_trx_id(view, trx_id)) {
			......
			break;
		}
		// 若是當前row版本不符合,則繼續回溯上一個版本(回到for循環的地方)
		version = prev_version;
	}
	......
}

<p>整個過程以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/1cfd4085657b2acd15f6b932ae091b5549d.jpg" alt="" /><br /> 至於undolog怎麼恢復出對應版本的row記錄就又是一個複雜的過程了,因爲篇幅緣由,在此略過不表。</p> <h2>read_view建立時機再討論</h2> <p>在建立一致性視圖的row_search_for_mysql的代碼中</p>

// 只有非鎖模式的select才建立一致性視圖
else if (prebuilt-&gt;select_lock_type == LOCK_NONE) {		// 建立一致性視圖
		trx_assign_read_view(trx);
		prebuilt-&gt;sql_stat_start = FALSE;
}

<p>trx_assign_read_view中由這麼一段代碼</p>

// 一致性視圖在一個事務只建立一次
if (!trx-&gt;read_view) {
		trx-&gt;read_view = read_view_open_now(
			trx-&gt;id, trx-&gt;global_read_view_heap);
		trx-&gt;global_read_view = trx-&gt;read_view;
	}

<p>因此綜合這兩段代碼,即在一個事務中,只有第一次運行select(不加鎖)的時候纔會建立一致性視圖,以下圖所示: <img src="https://oscimg.oschina.net/oscnet/dee1c8ba427aba23e0f82895acab16d47e3.jpg" alt="" /><br /> 筆者構造了此種場景模擬過,確實如此。</p> <h1>MVCC和鎖的同時做用致使的一些現象</h1> <p>MySQL是經過MVCC和二階段鎖(2PL)來兼顧性能和一致性的,可是因爲MySQL僅僅在select時候才建立一致性視圖,而在update等加鎖操做的時候並不作如此操做,因此就會產生一些詭異的現象。以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/e332ebdc3cc0bc6de027e873e6c86f52993.jpg" alt="" /><br /> 若是理解了update不走一致性視圖(read_view),而select走一致性視圖(read_view),就能夠很好解釋這個現象。 以下圖所示:<br /> <img src="https://oscimg.oschina.net/oscnet/ef2ef175797b612e6ebe93fc3b35f8e08f2.jpg" alt="" /></p> <h1>總結</h1> <p>MySQL爲了兼顧性能和ACID使用了大量複雜的機制,2PL(兩階段鎖)和MVCC就是其實現的典型。幸虧能夠經過xcode等IDE進行方便的debug,這樣就能夠很是精確加便捷的追蹤其各類機制的實現。但願這篇文章可以幫助到喜歡研究MySQL源碼的讀者們。</p> <h1>原文連接</h1> <p><a href="https://my.oschina.net/alchemystar/blog/1927425">https://my.oschina.net/alchemystar/blog/1927425</a></p>

原文連接:https://my.oschina.net/alchemystar/blog/1927425

相關文章
相關標籤/搜索