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 主要包含以下步驟
- mlog 中日誌刷盤
- 釋放 mtr 持有的鎖,鎖資訊儲存在 memo 中,以棧形式儲存,後加的鎖先釋放
- 清理 mtr 申請的記憶體空間,memo 和 log
- mtr—>state 設定為 MTR_COMMITTED
上面的步驟 1. 中,日誌刷盤策略和 innodb_flush_log_at_trx_commit 有關
當設定該值為1時,每次事務提交都要做一次fsync,這是最安全的配置,即使宕機也不會丟失事務
當設定為2時,則在事務提交時只做write操作,只保證寫到系統的page cache,因此例項crash不會丟失事務,但宕機則可能丟失事務
當設定為0時,事務提交不會觸發redo寫操作,而是留給後臺執行緒每秒一次的刷盤操作,因此例項crash將最多丟失1秒鐘內的事務
這篇也算是上篇 insert 執行過程的一個補充。
參考: