1. 程式人生 > >MySQL · 引擎特性 · InnoDB mini transation

MySQL · 引擎特性 · InnoDB mini transation

一 序

    之前的在整理redo log  redo log用來保證事務永續性,通過undo log可以看到資料較早版本,實現MVCC,或回滾事務等功能。

二 mini transaction 簡介

     innodb儲存引擎中的一個很重要的用來保證永續性的機制就是mini事務,在原始碼中用mtr(Mini-transaction)來表示,本書把它稱做“物理事務”,這樣叫是相對邏輯事務而言的,對於邏輯事務,做熟悉資料庫的人都很清楚,它是資料庫區別於檔案系統的最重要特性之一,它具有四個特性ACID,用來保證資料庫的完整性——要麼都做修改,要麼什麼都沒有做。物理事務從名字來看,是物理的,因為在innodb儲存引擎中,只要是涉及到檔案修改,檔案讀取等物理操作的,都離不開這個物理事務,可以說物理事務是記憶體與檔案之間的一個橋樑。

    

mini transation 主要用於innodb redo log 和 undo log寫入,保證兩種日誌的ACID特性

mini-transaction遵循以下三個協議:
The FIX Rules
Write-Ahead Log
Force-log-at-commit

The FIX Rules
修改一個頁需要獲得該頁的x-latch
訪問一個頁是需要獲得該頁的s-latch或者x-latch
持有該頁的latch直到修改或者訪問該頁的操作完成

Write-Ahead Log
持久化一個數據頁之前,必須先將記憶體中相應的日誌頁持久化
每個頁有一個LSN,每次頁修改需要維護這個LSN,當一個頁需要寫入到持久化裝置時,要求記憶體中小於該頁LSN的日誌先寫入到持久化裝置中

Force-log-at-commit
一個事務可以同時修改了多個頁,Write-AheadLog單個數據頁的一致性,無法保證事務的永續性
Force -log-at-commit要求當一個事務提交時,其產生所有的mini-transaction日誌必須刷到持久裝置中
這樣即使在頁資料刷盤的時候宕機,也可以通過日誌進行redo恢復

三 原始碼簡介

  本文使用 MySQL 5.7.18 版本進行分析

mini transation 相關程式碼路徑位於 storage/innobase/mtr/ 主要有 mtr0mtr.cc 和 mtr0log.cc 兩個檔案
另有部分程式碼在 storage/innobase/include/ 檔名以 mtr0 開頭.

mini transaction 的資訊儲存在結構體 mtr_t 中,原始碼在/innobase/include/mtr0mtr.h

/** Mini-transaction handle and buffer */
struct mtr_t {
 
	/** State variables of the mtr */
	struct Impl {
 
		/** memo stack for locks etc. */
		mtr_buf_t	m_memo;
 
		/** mini-transaction log */
		mtr_buf_t	m_log;
 
		/** true if mtr has made at least one buffer pool page dirty */
		bool		m_made_dirty;
 
		/** true if inside ibuf changes */
		bool		m_inside_ibuf;
 
		/** true if the mini-transaction modified buffer pool pages */
		bool		m_modifications;
 
		/** Count of how many page initial log records have been
		written to the mtr log */
		ib_uint32_t	m_n_log_recs;
 
		/** specifies which operations should be logged; default
		value MTR_LOG_ALL */
		mtr_log_t	m_log_mode;
#ifdef UNIV_DEBUG
		/** Persistent user tablespace associated with the
		mini-transaction, or 0 (TRX_SYS_SPACE) if none yet */
		ulint		m_user_space_id;
#endif /* UNIV_DEBUG */
		/** User tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_user_space;
		/** Undo tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_undo_space;
		/** System tablespace if it is being modified by the
		mini-transaction */
		fil_space_t*	m_sys_space;
 
		/** State of the transaction */
		mtr_state_t	m_state;
 
		/** Flush Observer */
		FlushObserver*	m_flush_observer;
 
#ifdef UNIV_DEBUG
		/** For checking corruption. */
		ulint		m_magic_n;
#endif /* UNIV_DEBUG */
 
		/** Owning mini-transaction */
		mtr_t*		m_mtr;
	};
變數名 描述
mtr_buf_t m_memo 用於儲存該mtr持有的鎖型別
mtr_buf_t m_log 儲存redo log記錄
bool m_made_dirty 是否產生了至少一個髒頁
bool m_inside_ibuf 是否在操作change buffer
bool m_modifications 是否修改了buffer pool page
ib_uint32_t m_n_log_recs 該mtr log記錄個數
mtr_log_t m_log_mode Mtr的工作模式,包括四種: MTR_LOG_ALL:預設模式,記錄所有會修改磁碟資料的操作;MTR_LOG_NONE:不記錄redo,髒頁也不放到flush list上;MTR_LOG_NO_REDO:不記錄redo,但髒頁放到flush list上;MTR_LOG_SHORT_INSERTS:插入記錄操作REDO,在將記錄從一個page拷貝到另外一個新建的page時用到,此時忽略寫索引資訊到redo log中。(參閱函式page_cur_insert_rec_write_log)
fil_space_t* m_user_space 當前mtr修改的使用者表空間
fil_space_t* m_undo_space 當前mtr修改的undo表空間
fil_space_t* m_sys_space 當前mtr修改的系統表空間
mtr_state_t m_state 包含四種狀態: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED

在修改或讀一個數據檔案中的資料時,一般是通過mtr來控制對對應page或者索引樹的加鎖,在5.7中,有以下幾種鎖型別(mtr_memo_type_t):

變數名 描述
MTR_MEMO_PAGE_S_FIX 用於PAGE上的S鎖
MTR_MEMO_PAGE_X_FIX 用於PAGE上的X鎖
MTR_MEMO_PAGE_SX_FIX 用於PAGE上的SX鎖,以上鎖通過mtr_memo_push 儲存到mtr中
MTR_MEMO_BUF_FIX PAGE上未加讀寫鎖,僅做buf fix
MTR_MEMO_S_LOCK S鎖,通常用於索引鎖
MTR_MEMO_X_LOCK X鎖,通常用於索引鎖
MTR_MEMO_SX_LOCK SX鎖,通常用於索引鎖,以上3個鎖,通過mtr_s/x/sx_lock加鎖,通過mtr_memo_release釋放鎖

四 一條insert語句涉及的 mini transaction

     InnoDB的redo log都是通過mtr產生的,先寫到mtr的cache中,然後再提交到公共buffer中,本小節以INSERT一條記錄對page產生的修改為例,闡述一個mtr的典型生命週期。關於insert 的執行過程,參見之前整理的https://blog.csdn.net/bohu83/article/details/82903976
    入口函式在row_ins_clust_index_entry_low,innobase/row/row0ins.cc

開啟MTR

row_ins_clust_index_entry_low(
/*==========================*/
	ulint		flags,	/*!< in: undo logging and locking flags */
	ulint		mode,	/*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE,
				depending on whether we wish optimistic or
				pessimistic descent down the index tree */
	dict_index_t*	index,	/*!< in: clustered index */
	ulint		n_uniq,	/*!< in: 0 or index->n_uniq */
	dtuple_t*	entry,	/*!< in/out: index entry to insert */
	ulint		n_ext,	/*!< in: number of externally stored columns */
	que_thr_t*	thr,	/*!< in: query thread */
	bool		dup_chk_only)
				/*!< in: if true, just do duplicate check
				and return. don't execute actual insert. */
{
	btr_pcur_t	pcur;
	btr_cur_t*	cursor;
	dberr_t		err		= DB_SUCCESS;
	big_rec_t*	big_rec		= NULL;
	mtr_t		mtr;
	mem_heap_t*	offsets_heap	= NULL;
	ulint           offsets_[REC_OFFS_NORMAL_SIZE];
	ulint*          offsets         = offsets_;
	rec_offs_init(offsets_);

	DBUG_ENTER("row_ins_clust_index_entry_low");

	ut_ad(dict_index_is_clust(index));
	ut_ad(!dict_index_is_unique(index)
	      || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!n_uniq || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!thr_get_trx(thr)->in_rollback);

	mtr_start(&mtr);
	mtr.set_named_space(index->space);

	if (dict_table_is_temporary(index->table)) {
		/* Disable REDO logging as the lifetime of temp-tables is
		limited to server or connection lifetime and so REDO
		information is not needed on restart for recovery.
		Disable locking as temp-tables are local to a connection. */

		ut_ad(flags & BTR_NO_LOCKING_FLAG);
		ut_ad(!dict_table_is_intrinsic(index->table)
		      || (flags & BTR_NO_UNDO_LOG_FLAG));

		mtr.set_log_mode(MTR_LOG_NO_REDO);
	}
...

mtr_start(&mtr);
mtr.set_named_space(index->space);
就是開啟mtr。
mtr_start主要包括:

  • 初始化mtr的各個狀態變數
  • 預設模式為MTR_LOG_ALL,表示記錄所有的資料變更
  • mtr狀態設定為ACTIVE狀態(MTR_STATE_ACTIVE)
  • 為鎖管理物件和日誌管理物件初始化記憶體(mtr_buf_t),初始化物件連結串列

   mtr.set_named_space 是5.7新增的邏輯,將當前修改的表空間物件fil_space_t儲存下來:如果是系統表空間,則賦值給m_impl.m_sys_space, 否則賦值給m_impl.m_user_space。
在5.7裡針對臨時表做了優化,直接關閉redo記錄: mtr.set_log_mode(MTR_LOG_NO_REDO)

定位插入位置

if (mode == BTR_MODIFY_LEAF && dict_index_is_online_ddl(index)) {
		mode = BTR_MODIFY_LEAF | BTR_ALREADY_S_LATCHED;
		mtr_s_lock(dict_index_get_lock(index), &mtr);
	}

	/* Note that we use PAGE_CUR_LE as the search mode, because then
	the function will return in both low_match and up_match of the
	cursor sensible values */
	btr_pcur_open(index, entry, PAGE_CUR_LE, mode, &pcur, &mtr);
	cursor = btr_pcur_get_btr_cur(&pcur);
	cursor->thr = thr;

	ut_ad(!dict_table_is_intrinsic(index->table)
	      || cursor->page_cur.block->made_dirty_with_no_latch);

#ifdef UNIV_DEBUG
	{
		page_t*	page = btr_cur_get_page(cursor);
		rec_t*	first_rec = page_rec_get_next(
			page_get_infimum_rec(page));

		ut_ad(page_rec_is_supremum(first_rec)
		      || rec_n_fields_is_sane(index, first_rec, entry));
	}
#endif /* UNIV_DEBUG */
...

    btr_pcur_open方法,獲取到這個新生成的index到底放到btr的哪個位置。這個位置,就由Cursor來標記標記。pcur是persistent cursor。因為btr是會分裂和變動的,當btr被分裂時,cursor的位置也會對應的進行變化。因此通過一層pcur的封裝,將cursor的變化對外遮蔽,針對一個index,我們只需要通過一個固定的pcur去獲取當前的cursor就可以了.(btr_pcur_open_low->btr_cur_search_to_nth_level)

獲取到了真實的cursor後,就可以拿到對應的leaf節點,就是具體的page。就是btr_cur_get_page。

   我們看看btr_cur_search_to_nth_level 對應的原始碼在 storage/innobase/btr/btr0cur.cc

函式的主要作用是將cursor移動到索引上待插入的位置,不展開看。

     不管插入還是更新操作,都是先以樂觀方式進行,因此先加索引S鎖 mtr_s_lock(dict_index_get_lock(index),&mtr),對應mtr_t::s_lock函式 如果以悲觀方式插入記錄,意味著可能產生索引分裂,在5.7之前會加索引X鎖,而5.7版本則會加SX鎖(但某些情況下也會退化成X鎖) 加X鎖: mtr_x_lock(dict_index_get_lock(index), mtr),對應mtr_t::x_lock函式 加SX鎖:mtr_sx_lock(dict_index_get_lock(index),mtr),對應mtr_t::sx_lock函式,原始碼在 storage/innobase/include/mtr0mtr.ic

/**
Locks a lock in x-mode. */

void
mtr_t::x_lock(rw_lock_t* lock, const char* file, ulint line)
{
	rw_lock_x_lock_inline(lock, 0, file, line);

	memo_push(lock, MTR_MEMO_X_LOCK);
}

/**
Locks a lock in sx-mode. */

void
mtr_t::sx_lock(rw_lock_t* lock, const char* file, ulint line)
{
	rw_lock_sx_lock_inline(lock, 0, file, line);

	memo_push(lock, MTR_MEMO_SX_LOCK);
}

    實際上就是加上對應的鎖物件,然後將該鎖的指標和型別構建的mtr_memo_slot_t物件插入到mtr.m_impl.m_memo中。
當找到預插入page對應的block,還需要加block鎖,並把對應的鎖型別加入到mtr:mtr_memo_push(mtr, block, fix_type)
   如果對page加的是MTR_MEMO_PAGE_X_FIX或者MTR_MEMO_PAGE_SX_FIX鎖,並且當前block是clean的,則將m_impl.m_made_dirty設定成true,表示即將修改一個乾淨的page。
     如果加鎖型別為MTR_MEMO_BUF_FIX,實際上是不加鎖物件的,但需要判斷臨時表的場景,臨時表page的修改不加latch,但需要將m_impl.m_made_dirty設定為true(根據block的成員m_impl.m_made_dirty來判斷),這也是5.7對InnoDB臨時表場景的一種優化。
    同樣的,根據鎖型別和鎖物件構建mtr_memo_slot_t加入到m_impl.m_memo中。

插入資料

    先進性樂觀插入,失敗在執行悲觀插入。

err = btr_cur_optimistic_insert(
				flags, cursor,
				&offsets, &offsets_heap,
				entry, &insert_rec, &big_rec,
				n_ext, thr, &mtr);

			if (err == DB_FAIL) {
				err = btr_cur_pessimistic_insert(
					flags, cursor,
					&offsets, &offsets_heap,
					entry, &insert_rec, &big_rec,
					n_ext, thr, &mtr);
			}

在插入資料過程中,包含大量的redo寫cache邏輯,例如更新二級索引頁的max trx id、寫undo log產生的redo(巢狀另外一個mtr)、修改資料頁產生的日誌。這裡我們只討論修改資料頁產生的日誌,進入函式page_cur_insert_rec_write_log:原始碼在innobase/page/page0cur.cc。這裡不貼了。

Step 1: 呼叫函式mlog_open_and_write_index記錄索引相關資訊
Step 2: 寫入記錄在page上的偏移量,佔兩個位元組
mach_write_to_2(log_ptr, page_offset(cursor_rec));
Step 3: 寫入記錄其它相關資訊 (rec size, extra size, info bit,關於InnoDB的資料檔案物理描述,參見淘寶資料庫月報)
Step 4: 將插入的記錄拷貝到redo檔案,同時關閉mlog
memcpy(log_ptr, ins_ptr, rec_size);
mlog_close(mtr, log_ptr + rec_size);

   通過上述流程,我們寫入了一個型別為MLOG_COMP_REC_INSERT的日誌記錄。由於特定型別的記錄都基於約定的格式,在崩潰恢復時也可以基於這樣的約定解析出日誌。

  更多的redo log記錄型別參見enum mlog_id_t  原始碼在innobase/include/mtr0types.h
在這個過程中產生的redo log都記錄在mtr.m_impl.m_log中,只有顯式提交mtr時,才會寫到公共buffer中。

提交MTR log

  當提交一個mini transaction時,需要將對資料的更改記錄提交到公共buffer中,並將對應的髒頁加到flush list上。
入口函式為mtr_t::commit(),當修改產生髒頁或者日誌記錄時,呼叫mtr_t::Command::execute 原始碼在innobase/mtr/mtr0mtr.cc 

/** Write the redo log record, add dirty pages to the flush list and release
the resources. */
void
mtr_t::Command::execute()
{
	ut_ad(m_impl->m_log_mode != MTR_LOG_NONE);
 
	if (const ulint len = prepare_write()) {
		finish_write(len);
	}
 
	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_enter();
	}
 
	/* It is now safe to release the log mutex because the
	flush_order mutex will ensure that we are the first one
	to insert into the flush list. */
	log_mutex_exit();
 
	m_impl->m_mtr->m_commit_lsn = m_end_lsn;
 
	release_blocks();
 
	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_exit();
	}
 
	release_latches();
 
	release_resources();
}

Step 1: mtr_t::Command::prepare_write()
     主要是持有log_sys->mutex,做寫入前檢查
Step 2: mtr_t::Command::finish_write
將日誌從mtr中拷貝到公共log buffer。
Step 3:如果本次修改產生了髒頁,獲取log_sys->log_flush_order_mutex,隨後釋放log_sys->mutex。
Step 4. 將當前Mtr修改的髒頁加入到flush list上,髒頁上記錄的lsn為當前mtr寫入的結束點lsn。基於上述加鎖邏輯,能夠保證flush list上的髒頁總是以LSN排序。
Step 5. 釋放log_sys->log_flush_order_mutex鎖
Step 6. 釋放當前mtr持有的鎖(主要是page latch)及分配的記憶體,mtr完成提交。

至此 insert 語句涉及的 mini transaction 全部結束.

五 總結

上面可以看到加鎖、寫日誌到 mlog 等操作在 mini transaction 過程中進行。解鎖、把日誌刷盤等操作全部在 mtr_commit 中進行,和事務類似。mini transaction 沒有回滾操作, 因為只有在 mtr_commit 才將修改落盤,如果宕機,記憶體丟失,無需回滾;如果落盤過程中宕機,崩潰恢復時可以看出落盤過程不完整,丟棄這部分修改。
mtr_commit 主要包含以下步驟

  1. mlog 中日誌刷盤
  2. 釋放 mtr 持有的鎖,鎖資訊儲存在 memo 中,以棧形式儲存,後加的鎖先釋放
  3. 清理 mtr 申請的記憶體空間,memo 和 log
  4. mtr—>state 設定為 MTR_COMMITTED

上面的步驟 1. 中,日誌刷盤策略和 innodb_flush_log_at_trx_commit 有關
當設定該值為1時,每次事務提交都要做一次fsync,這是最安全的配置,即使宕機也不會丟失事務
當設定為2時,則在事務提交時只做write操作,只保證寫到系統的page cache,因此例項crash不會丟失事務,但宕機則可能丟失事務
當設定為0時,事務提交不會觸發redo寫操作,而是留給後臺執行緒每秒一次的刷盤操作,因此例項crash將最多丟失1秒鐘內的事務

這篇也算是上篇 insert 執行過程的一個補充。

參考:

http://mysql.taobao.org/monthly/2017/10/03/

http://mysql.taobao.org/monthly/2015/05/01