網(wǎng)站開發(fā)配置狀態(tài)統(tǒng)計seo標(biāo)題優(yōu)化褲子關(guān)鍵詞
基礎(chǔ)概念
并發(fā)事務(wù)帶來的問題
1)臟讀:一個事務(wù)讀取到另一個事務(wù)更新但還未提交的數(shù)據(jù),如果另一個事務(wù)出現(xiàn)回滾或者進(jìn)一步更新,則會出現(xiàn)問題。
2)不可重復(fù)讀:在一個事務(wù)中兩次次讀取同一個數(shù)據(jù)時,由于在兩次讀取之間,另一個事務(wù)修改了該數(shù)據(jù),所以出現(xiàn)兩次讀取的結(jié)果不一致。
3)幻讀:在一個事務(wù)中使用相同的 SQL 兩次讀取,第二次讀取到了其他事務(wù)新插入的行。
要解決這些并發(fā)事務(wù)帶來的問題,一個比較簡單粗暴的方法是加鎖,但是加鎖必然會帶來性能的降低,因此 MySQL 使用了 MVCC 來提升并發(fā)事務(wù)下的性能。
MVCC 帶來的好處
如果沒有 MVCC,為了保證并發(fā)事務(wù)的安全,一個比較容易想到的辦法就是加讀寫鎖,實現(xiàn):讀讀不沖突、讀寫沖突、寫讀沖突,寫寫沖突,在這種情況下,并發(fā)讀寫的性能必然會收到嚴(yán)重影響。
而通過 MVCC,我們可以做到讀寫之間不沖突,我們讀的時候只需要將當(dāng)前記錄拷貝一份到內(nèi)存中(ReadView),之后該事務(wù)的查詢就只跟 ReadView 打交道,不影響其他事務(wù)對該記錄的寫操作。
事務(wù)隔離級別
1)讀未提交(Read Uncommitted):最低的隔離級別,會讀取到其他事務(wù)還未提交的內(nèi)容,存在臟讀。
2)讀已提交(Read Committed):讀取到的內(nèi)容都是已經(jīng)提交的,可以解決臟讀,但是存在不可重復(fù)讀。
3)可重復(fù)讀(Repeatable Read):在一個事務(wù)中多次讀取時看到相同的內(nèi)容,可以解決不可重復(fù)讀,但是存在幻讀。在 InnoDB 中不存在幻讀問題,對于快照讀,InnoDB 使用 MVCC 解決幻讀,對于當(dāng)前讀,InnoDB 通過 gap locks 或 next-key locks 解決幻讀。
4)串行化(Serializable):最高的隔離級別,串行的執(zhí)行事務(wù),沒有并發(fā)事務(wù)問題。
InnoDB MVCC 實現(xiàn)
核心數(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<ReadView *>(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;
?// 省略...
};
基本理論基礎(chǔ)
當(dāng)前讀和快照讀
當(dāng)前讀:官方叫做 Locking Reads(鎖定讀取),讀取數(shù)據(jù)的最新版本。常見的 update/insert/delete、還有 select ... for update、select ... lock in share mode 都是當(dāng)前讀。
官方文檔:MySQL :: MySQL 8.0 Reference Manual :: 15.7.2.4 Locking Reads
快照讀:官方叫做 Consistent Nonlocking Reads(一致性非鎖定讀取,也叫一致性讀取),讀取快照版本,也就是 MVCC 生成的 ReadView。用于普通的 select 的語句。MySQL 默認(rèn)快照讀。
官方文檔:MySQL :: MySQL 8.0 Reference Manual :: 15.7.2.3 Consistent Nonlocking Reads
低水位線與高水位線
- m_up_limit_id:低水位線,活躍事務(wù)的最小 ID
- m_low_limit_id:高水位線,活躍事務(wù)的最大 ID + 1
當(dāng)一個事務(wù)修改一條記錄時,會將該事務(wù) ID 插入到當(dāng)前記錄,并存儲與 DB_TRX_ID 字段中。
活躍事務(wù)是指在創(chuàng)建 ReadView 時,啟動但未提交的事務(wù),MySQL 源碼中使用 m_ids(m_ids 中的元素遞增存儲的)結(jié)構(gòu)保存活躍事務(wù) ID。
事務(wù)可見性與高低水位線的關(guān)系:
1)已提交事務(wù)對當(dāng)前事務(wù)一定可見;
2)未開始事務(wù)對當(dāng)前事務(wù)一定不可見;
3)當(dāng)事務(wù) ID 介于 m_up_limit_id 和 m_low_limit_id 之間時,需判斷事務(wù) ID 是否位于 m_ids 中:
- 如果事務(wù) ID 位于 m_ids 中,則對當(dāng)前事務(wù)不可見,因為事務(wù)還未提交
- 否則,對當(dāng)前事務(wù)可見
增加隱藏字段
為了實現(xiàn) MVCC,InnoDB 會向表中添加三個隱藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。
- DB_ROW_ID:該字段占 6 個字節(jié),在建表時如果沒有顯示指定主鍵,InnoDB 會首先判斷該表中是否有非空唯一索引,如果有,則該索引即為主鍵并將主鍵值存放到該字段中(如果存在多個非空唯一索引,則選擇第一個創(chuàng)建的非空唯一索引為主鍵);如果沒有非空唯一索引,InnoDB 會隱式生成主鍵并存儲于當(dāng)前字段中;
- DB_TRX_ID:該字段占 6 個字節(jié),用于存儲最近修改或插入當(dāng)前記錄的事務(wù)ID;
- DB_ROLL_PTR:該字段占 7 個字節(jié),指向?qū)懭牖貪L段的 undo log 記錄。每次對某條記錄進(jìn)行更新時,會通過 undo log 記錄更新前的行記錄內(nèi)容,更新后的行記錄會通過 DB_ROLL_PTR 指向該 undo log 。當(dāng)某條記錄被多次修改時,該行記錄會存在多個版本,通過 DB_ROLL_PTR 鏈接形成一個類似版本鏈的概念,大致如下圖所示。
說明:隱藏列 DB_ROW_ID 是早期 MySQL 版本的叫法,在后續(xù)版本中,為了與應(yīng)用程序更好地兼容,這個列的名稱被更改為?_rowid。
隱藏字段源碼解析
上述三個隱藏字段在 MySQL 源碼 .\mysql\storage\innobase\dict\dict0dict.cc 文件的 dict_table_add_system_columns 函數(shù)中,具體函數(shù)內(nèi)容如下:
/** Adds system columns to a table object. // 添加系統(tǒng)列到表對象
@param[in,out] table Table
@param[in] heap Temporary heap */
void dict_table_add_system_columns(dict_table_t *table, mem_heap_t *heap) {ut_ad(table);ut_ad(table->n_def == (table->n_cols - table->get_n_sys_cols()));ut_ad(table->magic_n == DICT_TABLE_MAGIC_N);ut_ad(!table->cached);/* NOTE: the system columns MUST be added in the following order(so that they can be indexed by the numerical value of DATA_ROW_ID,etc.) and as the last columns of the table memory object.The clustered index will not always physically contain all systemcolumns.Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned offfor these tables. */// 添加主鍵 IDdict_mem_table_add_col(table, heap, "DB_ROW_ID", DATA_SYS,DATA_ROW_ID | DATA_NOT_NULL, DATA_ROW_ID_LEN, false);// 添加事務(wù) IDdict_mem_table_add_col(table, heap, "DB_TRX_ID", DATA_SYS,DATA_TRX_ID | DATA_NOT_NULL, DATA_TRX_ID_LEN, false);if (!table->is_intrinsic()) { // 判斷是否是內(nèi)部表// 添加回滾指針dict_mem_table_add_col(table, heap, "DB_ROLL_PTR", DATA_SYS,DATA_ROLL_PTR | DATA_NOT_NULL, DATA_ROLL_PTR_LEN,false);/* This check reminds that if a new system column is added tothe program, it should be dealt with here */}
}
增刪改的底層操作
更新操作
當(dāng)我們更新一條數(shù)據(jù)時,InnoDB 會進(jìn)行如下操作:
- 加鎖:對要更新的行記錄加排他鎖;
- 寫 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。
更新記錄源碼解析
更新行記錄在 MySQL 源碼 .\mysql\storage\innobase\btr\btr0cur.cc 文件的 btr_cur_update_in_place 函數(shù)中,具體函數(shù)內(nèi)容如下:
/** Updates a record when the update causes no size changes in its fields. */
dberr_t btr_cur_update_in_place(ulint flags, btr_cur_t *cursor, ulint *offsets,const upd_t *update, ulint cmpl_info,que_thr_t *thr, trx_id_t trx_id, mtr_t *mtr) {// 省略...// 通過 B+ 樹游標(biāo)獲取當(dāng)前記錄rec = btr_cur_get_rec(cursor);index = cursor->index; // 或者當(dāng)前游標(biāo)所指的索引// 省略...// 寫 undo logerr = btr_cur_upd_lock_and_undo(flags, cursor, offsets, update, cmpl_info,thr, mtr, &roll_ptr);// 省略...if (!(flags & BTR_KEEP_SYS_FLAG) && !index->table->is_intrinsic()) {// 更新 rec 的 trx_id、roll_ptr 屬性值row_upd_rec_sys_fields(rec, nullptr, index, offsets, thr_get_trx(thr),roll_ptr);}// 獲取記錄的刪除標(biāo)記,如果記錄被標(biāo)記為刪除,則返回 truewas_delete_marked =rec_get_deleted_flag(rec, page_is_comp(buf_block_get_frame(block)));// 省略...// 將 rec 要更新的屬性列更新為目標(biāo)值row_upd_rec_in_place(rec, index, offsets, update, page_zip);// 寫 redo logbtr_cur_update_in_place_log(flags, rec, index, update, trx_id, roll_ptr, mtr);// 如果當(dāng)前記錄被標(biāo)記為刪除且還沒有被刪除,則取消刪除標(biāo)記,因為字段的所有權(quán)已經(jīng)轉(zhuǎn)移到了新記錄上if (was_delete_marked &&!rec_get_deleted_flag(rec, page_is_comp(buf_block_get_frame(block)))) {/* The new updated record owns its possible externallystored fields */lob::BtrContext btr_ctx(mtr, nullptr, index, rec, offsets, block);btr_ctx.unmark_extern_fields();}// 省略...return (err);
}
構(gòu)建一致性讀取視圖(ReadView)
當(dāng)我們的隔離級別為 RR 時:每開啟一個事務(wù),系統(tǒng)會給該事務(wù)會分配一個事務(wù) Id,在該事務(wù)執(zhí)行第一個 select 語句的時候,會生成一個當(dāng)前時間點的事務(wù)快照 ReadView,核心屬性如下:
- m_ids:創(chuàng)建 ReadView 時當(dāng)前系統(tǒng)中活躍的事務(wù) Id 列表,可以理解為生成 ReadView 那一刻還未執(zhí)行提交的事務(wù),并且該列表是個升序列表。
- m_up_limit_id:低水位(小于低水位的事務(wù)都可見),取 m_ids 列表的第一個節(jié)點,因為 m_ids 是升序列表,因此也就是 m_ids 中事務(wù) Id 最小的那個。
- m_low_limit_id:高水位(大于等于高水位的事務(wù)都不可見),生成 ReadView 時系統(tǒng)將要分配給下一個事務(wù)的 Id 值(即 m_ids 列表的最后一個節(jié)點加 1)。
- m_creator_trx_id:創(chuàng)建 ReadView 的事務(wù) Id。
一致性讀取視圖源碼解析
一致性讀取視圖在 MySQL 源碼 .\mysql\storage\innobase\read\read0read.cc 文件的 ReadView::prepare 函數(shù)中,具體函數(shù)內(nèi)容如下:
調(diào)用堆棧:row_search_mvcc => trx_assign_read_view => MVCC::view_open => ReadView::prepare
/**
Opens a read view where exactly the transactions serialized before this
point in time are seen in the view.
@param id Creator transaction id */void ReadView::prepare(trx_id_t id) {ut_ad(trx_sys_mutex_own());m_creator_trx_id = id; // 創(chuàng)建 ReadView 的事務(wù) IDm_low_limit_no = trx_get_serialisation_min_trx_no(); // 視圖不需要查看小于該值的事務(wù) ID// 高水位m_low_limit_id = trx_sys_get_next_trx_id_or_no();ut_a(m_low_limit_no <= m_low_limit_id);// 如果事務(wù)系統(tǒng)的活躍事務(wù)列表不為空,則將其拷貝至 m_idsif (!trx_sys->rw_trx_ids.empty()) {/*1) copy_trx_ids 函數(shù)通過調(diào)用 m_up_limit_id = m_ids.front() 將 m_ids 中的最小事務(wù) id 賦值給 m_up_limit_id;2) 使用二分查找算法,查找 trx_id 是否在 m_ids 中;*/ copy_trx_ids(trx_sys->rw_trx_ids);} else { // 否則清空 ReadView 的活躍事務(wù)列表m_ids.clear();}/* The first active transaction has the smallest id. */m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;ut_a(m_up_limit_id <= m_low_limit_id);ut_d(m_view_low_limit_no = m_low_limit_no);m_closed = false;
}
最后,會將這個創(chuàng)建的 ReadView 添加到 MVCC 的 m_views 中。
一致性視圖可見性判斷
SQL 查詢走聚簇索引
有了這個 ReadView,在訪問某條記錄時,只需要按照如下步驟即可判斷當(dāng)前記錄的某個版本是否可見:
1)如果被訪問記錄的 trx_id 與 ReadView 中 的 m_creator_trx_id 值相同,則意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該記錄可以被當(dāng)前事務(wù)訪問。
2)如果被訪問記錄的 trx_id 小于 ReadView 中的 m_up_limit_id(低水位),表明該版本記錄的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本記錄可以被當(dāng)前事務(wù)訪問。
3)如果被訪問記錄的 trx_id 大于等于 ReadView 中的 m_low_limit_id(高水位),表明該版本記錄的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
4)如果被訪問記錄的 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)被提交,該版本可以被訪問。
在進(jìn)行判斷時,首先會拿記錄的最新版本來比較,如果該版本記錄無法被當(dāng)前事務(wù)看到,則通過記錄的 DB_ROLL_PTR 找到上一個版本,重新進(jìn)行比較,直到找到一個能被當(dāng)前事務(wù)看到的版本。
而對于刪除,其實就是一種特殊的更新,InnoDB 在 info_bits 中用一個標(biāo)記位 delete_flag 標(biāo)識是否刪除。當(dāng)我們在進(jìn)行判斷時,會檢查下 delete_flag 是否被標(biāo)記,如果是,則會根據(jù)情況進(jìn)行處理:
- 如果索引是聚簇索引,并且具有唯一特性(主鍵、唯一索引等),則返回 DB_RECORD_NOT_FOUND;
- 否則,會尋找下一條記錄繼續(xù)流程;
其實很容易理解,如果是唯一索引查詢,必然只有一條記錄,如果被刪除了則直接返回空,而如果是普通索引,可能存在多個相同值的行記錄,該行不存在,則繼續(xù)查找下一條。
注意:以上內(nèi)容是對于 RR(可重復(fù)讀)級別來說,而對于 RC(讀提交)級別,其實整個過程幾乎一樣,唯一不同的是生成 ReadView 的時機(jī),RR 級別只在事務(wù)第一次 select 時生成一次,之后一直使用該 ReadView。而 RC 級別則在每次 select 時,都會生成一個 ReadView。
聚簇索引可見性源碼解析
走聚簇索引的核心流程在 row_search_mvcc 函數(shù)中,具體調(diào)用堆棧:row_search_mvcc => lock_clust_rec_cons_read_sees => changes_visible。
1)row_search_mvcc 函數(shù)
// 功能:使用游標(biāo)在數(shù)據(jù)庫中搜索行
dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode,row_prebuilt_t *prebuilt, ulint match_mode,const ulint direction) {// 省略.../*-------------------------------------------------------------*/// 階段1:嘗試從記錄緩沖區(qū)或預(yù)取緩存中彈出行// 省略.../*-------------------------------------------------------------*/// 階段2:如果可能的話,嘗試快速自適應(yīng)散列索引搜索// 省略.../*-------------------------------------------------------------*/// 階段3:打開或恢復(fù)索引游標(biāo)位置// 省略.../*-------------------------------------------------------------*//* 階段4:在一個循環(huán)中查找匹配的記錄 */// 省略...// 檢查隔離級別是否是讀未提交,如果是,什么也不做if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) { /* Do nothing: we let a non-locking SELECT read thelatest version of the record */} else if (index == clust_index) { /*------ 聚簇索引 ------*/ /* --------------------------------------------------- */// 判斷 rec 在 ReadView 中是否可見if (srv_force_recovery < 5 && !lock_clust_rec_cons_read_sees(rec, index, offsets,trx_get_read_view(trx))) { rec_t *old_vers;// 獲取 rec 的上一個版本,并將其賦值給 old_verserr = row_sel_build_prev_vers_for_mysql(trx->read_view, clust_index, prebuilt, rec, &offsets, &heap,&old_vers, need_vrow ? &vrow : nullptr, &mtr,prebuilt->get_lob_undo());if (err != DB_SUCCESS) {goto lock_wait_or_error;}// 如果舊版本中的行記錄也為空或不可見,則繼續(xù)處理下一條記錄if (old_vers == nullptr) {goto next_rec;}rec = old_vers; // 記錄指針更新為先前版本的記錄prev_rec = rec;}} else { /*------ 非聚簇索引 ------*/ /* --------------------------------------------------- */// 判斷 rec 在 ReadView 中的可見性if (!srv_read_only_mode &&!lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) {/* 注:lock_sec_rec_cons_read_sees 函數(shù)判斷可見性邏輯:page頁最大事務(wù)ID小于低水位,即可見;page頁最大事務(wù)ID大于等于低水位時認(rèn)為是不可見,但在goto requires_clust_rec時會進(jìn)一步判斷當(dāng)前記錄的可見性*/ switch (row_search_idx_cond_check(buf, prebuilt, rec, offsets)) {case ICP_NO_MATCH: // 不滿足二級索引條件,當(dāng)有多條記錄時排除其它記錄,icp下推二級索引過濾goto next_rec;case ICP_OUT_OF_RANGE: // 超出二級索引范圍err = DB_RECORD_NOT_FOUND;goto idx_cond_failed;case ICP_MATCH: // 滿足二級索引條件,通過回表查找當(dāng)前記錄的事務(wù)id,以獲取當(dāng)前事務(wù)的上一版本數(shù)據(jù)goto requires_clust_rec;}ut_error; // 出現(xiàn)了意外情況,拋出錯誤}}}/*-------------------------------------------------------------*//* 階段5:將光標(biāo)移動到下一個索引記錄 */// 省略...
}
2)lock_clust_rec_cons_read_sees 函數(shù)
/** Checks that a record is seen in a consistent read.@return true if sees, or false if an earlier version of the recordshould be retrieved */
bool lock_clust_rec_cons_read_sees(const rec_t *rec, /*!< in: user record which should be read orpassed over by a read cursor */ dict_index_t *index, /*!< in: clustered index */const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */ReadView *view) /*!< in: consistent read view */
{ut_ad(index->is_clustered()); ut_ad(page_rec_is_user_rec(rec)); ut_ad(rec_offs_validate(rec, index, offsets)); /* Temp-tables are not shared across connections and multipletransactions from different connections cannot simultaneouslyoperate on same temp-table and so read of temp-table isalways consistent read. *//* 如果數(shù)據(jù)庫處于只讀模式或者表是臨時的,則返回true,表示始終可以看到該記錄。這是因為臨時表在多個連接和事務(wù)中不是共享的,所以讀取總是具有一致性。*/if (srv_read_only_mode || index->table->is_temporary()) {ut_ad(view == nullptr || index->table->is_temporary());return (true);}/* NOTE that we call this function while holding the searchsystem latch. */// 獲取當(dāng)前記錄的事務(wù)ID// 主鍵索引掃描時,rec是一條完整記錄,可以直接獲取當(dāng)前記錄的事務(wù)id trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);// 判斷 ReadView 對當(dāng)前記錄的可見性return (view->changes_visible(trx_id, index->table->name));
}
3)changes_visible 函數(shù)
/** Check whether the changes by id are visible.
@param[in] id transaction id to check against the view
@param[in] name table name
@return whether the view sees the modifications of id. */
[[nodiscard]] bool changes_visible(trx_id_t id,const table_name_t &name) const {ut_ad(id > 0); // 事務(wù)id一般是正整數(shù),0 可能表示“沒有事務(wù)”或“無效事務(wù)”/* m_up_limit_id 為低水位線,小于低水位線的事務(wù)ID都可見m_creator_trx_id 為當(dāng)前事務(wù)id,記錄被當(dāng)前事務(wù)修改時也一定可見*/if (id < m_up_limit_id || id == m_creator_trx_id) {return (true);}// 檢查事務(wù)ID是否有效check_trx_id_sanity(id, name);// m_low_limit_id 為高水位線,所有大于高水位線的事務(wù)ID都不可見if (id >= m_low_limit_id) {return (false);// 判斷創(chuàng)建 ReadView 時的活躍事務(wù)列表是否為空,若為空,則當(dāng)前記錄可見} else if (m_ids.empty()) { return (true);}const ids_t::value_type *p = m_ids.data();// 使用二分查找在 m_ids 中查找當(dāng)前記錄的事務(wù)ID,若找到,則當(dāng)前記錄不可見;若未找到,則可見return (!std::binary_search(p, p + m_ids.size(), id));
}
獲取當(dāng)前記錄的上一個版本源碼解析
獲取當(dāng)前記錄的上一個版本,主要是通過 DB_ROLL_PTR 來實現(xiàn),核心流程如下:
1)獲取當(dāng)前記錄的回滾指針 DB_ROLL_PTR、獲取當(dāng)前記錄的事務(wù) ID;
2)通過回滾指針拿到對應(yīng)的 undo log;
3)解析 undo log,并使用 undo log 構(gòu)建用于更新向量 UPDATE;
4)構(gòu)建記錄的上一個版本:先用記錄的當(dāng)前版本填充,然后使用 UPDATE(undo log)進(jìn)行覆蓋。
獲取當(dāng)前記錄的上一個版本的核心流程在 row_search_mvcc 函數(shù)中,具體調(diào)用堆棧:row_search_mvcc => row_sel_build_prev_vers_for_mysql => row_vers_build_for_consistent_read => ?trx_undo_prev_version_build。
bool trx_undo_prev_version_build(const rec_t *index_rec ATTRIB_USED_ONLY_IN_DEBUG,mtr_t *index_mtr ATTRIB_USED_ONLY_IN_DEBUG, const rec_t *rec,const dict_index_t *const index, ulint *offsets, mem_heap_t *heap,rec_t **old_vers, mem_heap_t *v_heap, const dtuple_t **vrow, ulint v_status,lob::undo_vers_t *lob_undo) {// 省略...// 1) 獲取 rec 的回滾指針 roll_ptrroll_ptr = row_get_rec_roll_ptr(rec, index, offsets);*old_vers = nullptr;if (trx_undo_roll_ptr_is_insert(roll_ptr)) {/* The record rec is the first inserted version */return true;}// 2) 獲取 rec 的事務(wù) IDrec_trx_id = row_get_rec_trx_id(rec, index, offsets);/* REDO rollback segments are used only for non-temporary objects.For temporary objects NON-REDO rollback segments are used. */bool is_temp = index->table->is_temporary();ut_ad(!index->table->skip_alter_undo);// 3) 通過回滾指針獲取 undo log 記錄,拷貝到 heap 中,并且填充給 undo_recif (trx_undo_get_undo_rec(roll_ptr, rec_trx_id, heap, is_temp,index->table->name, &undo_rec)) {// 省略...}// 4) 開始解析 undo log,填充給對應(yīng)的參數(shù)(參考 undo log 的構(gòu)造圖,這邊會按構(gòu)造順序依次讀取填充)type_cmpl_t type_cmpl;// 4.1) 通過 undo_rec 讀取 type、undo_no、cmpl_info、table_id 的值,并填充到形參變量中// 讀取這些值后,將 undo log 的剩余部分返回,并賦值給 ptrptr = trx_undo_rec_get_pars(undo_rec, &type, &cmpl_info, &dummy_extern,&undo_no, &table_id, type_cmpl);// 省略...// 4.2) 從 ptr 讀取 trx_id、roll_ptr、info_bits 的值,并填充到形參參的變量中// 讀取這些值后,將 undo log 的剩余部分返回,并賦值給 ptrptr = trx_undo_update_rec_get_sys_cols(ptr, &trx_id, &roll_ptr, &info_bits);// 4.3) undo log 跳過行引用ptr = trx_undo_rec_skip_row_ref(ptr, index);// 4.4) 通過 undo log 的剩余部分(roll_ptr、info_bts等)構(gòu)建 update(就是填充到update變量中)ptr = trx_undo_update_rec_get_update(ptr, index, type, trx_id, roll_ptr,info_bits, nullptr, heap, &update,lob_undo, type_cmpl);ut_a(ptr);if (row_upd_changes_field_size_or_external(index, offsets, update)) {// 省略...} else {buf = static_cast<byte *>(mem_heap_alloc(heap, rec_offs_size(offsets)));// 5) 構(gòu)建 old_vers:先用 rec 填充,再用 update 覆蓋(也就是 rec 的 roll_ptr 指向的 undo 1og 的內(nèi)容)// 5.1) 將 rec 拷貝到 buf 中,同時返回指向 buf 數(shù)據(jù)部分的指針,賦值給 old_vers*old_vers = rec_copy(buf, rec, offsets);rec_offs_make_valid(*old_vers, index, offsets);// 5.2) 使用 update 覆蓋 old_vers,相當(dāng)于先將 old_vers 賦值為 rec(當(dāng)前版本),然后用 undo log 來覆蓋row_upd_rec_in_place(*old_vers, index, offsets, update, nullptr);}// 省略...
}
SQL 查詢走非聚簇索引
當(dāng)走普通索引時,判斷邏輯如下:
1)判斷被訪問索引記錄所在頁的最大事務(wù) Id 是否小于 ReadView 中的 m_up_limit_id(低水位),如果是,則代表該頁的最后一次修改事務(wù) ID 在 ReadView 創(chuàng)建前之前已經(jīng)提交,所以一定可以訪問;如果不是,并不代表一定不可以訪問,道理跟走聚簇索引一樣,事務(wù) ID 大的也可能提交比較早,所以需要做進(jìn)一步判斷,見步驟 2。
2)根據(jù)索引信息,使用 ICP(Index Condition Pushdown)來判斷搜索條件是否滿足,主要目的是,在使用聚簇索引判斷記錄可見性之前先進(jìn)行過濾(可以減少不必要的回表操作),這邊有三種情況:
- ICP 判斷不滿足條件但沒有超出掃描范圍,則獲取下一條記錄繼續(xù)查找;
- 如果不滿足條件并且超出掃描返回,則返回 DB_RECORD_NOT_FOUND;
- 如果 ICP 判斷符合條件,則會獲取對應(yīng)的聚簇索引來進(jìn)行可見性判斷。
說明:在進(jìn)行非聚簇索引可見性判斷時,若不可見,但滿足 ICP 條件,則會繼續(xù)調(diào)用 lock_clust_rec_cons_read_sees 函數(shù)再次判斷當(dāng)前記錄的可見性。
非聚簇索引可見性判斷源碼解析
走非聚簇索引的核心流程在 row_search_mvcc 函數(shù)中,具體調(diào)用堆棧:row_search_mvcc => lock_sec_rec_cons_read_seese。
1)row_search_mvcc 函數(shù)
// 功能:使用游標(biāo)在數(shù)據(jù)庫中搜索行
dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode,row_prebuilt_t *prebuilt, ulint match_mode,const ulint direction) {// 省略.../*-------------------------------------------------------------*/// 階段1:嘗試從記錄緩沖區(qū)或預(yù)取緩存中彈出行// 省略.../*-------------------------------------------------------------*/// 階段2:如果可能的話,嘗試快速自適應(yīng)散列索引搜索// 省略.../*-------------------------------------------------------------*/// 階段3:打開或恢復(fù)索引游標(biāo)位置// 省略.../*-------------------------------------------------------------*//* 階段4:在一個循環(huán)中查找匹配的記錄 */// 省略...// 檢查隔離級別是否是讀未提交,如果是,什么也不做if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) { /* Do nothing: we let a non-locking SELECT read thelatest version of the record */} else if (index == clust_index) { /*------ 聚簇索引 ------*/ /* --------------------------------------------------- */// 判斷 rec 在 ReadView 中是否可見if (srv_force_recovery < 5 && !lock_clust_rec_cons_read_sees(rec, index, offsets,trx_get_read_view(trx))) { rec_t *old_vers;// 獲取 rec 的上一個版本,并將其賦值給 old_verserr = row_sel_build_prev_vers_for_mysql(trx->read_view, clust_index, prebuilt, rec, &offsets, &heap,&old_vers, need_vrow ? &vrow : nullptr, &mtr,prebuilt->get_lob_undo());if (err != DB_SUCCESS) {goto lock_wait_or_error;}// 如果舊版本中的行記錄也為空或不可見,則繼續(xù)處理下一條記錄if (old_vers == nullptr) {goto next_rec;}rec = old_vers; // 記錄指針更新為先前版本的記錄prev_rec = rec;}} else { /*------ 非聚簇索引 ------*/ /* --------------------------------------------------- */// 判斷 rec 在 ReadView 中的可見性if (!srv_read_only_mode &&!lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) {/* 注:lock_sec_rec_cons_read_sees 函數(shù)判斷可見性邏輯:page頁最大事務(wù)ID小于低水位,即可見;page頁最大事務(wù)ID大于等于低水位時認(rèn)為是不可見,但在goto requires_clust_rec時會進(jìn)一步判斷當(dāng)前記錄的可見性*/ switch (row_search_idx_cond_check(buf, prebuilt, rec, offsets)) {case ICP_NO_MATCH: // 不滿足二級索引條件,當(dāng)有多條記錄時排除其它記錄,icp下推二級索引過濾goto next_rec;case ICP_OUT_OF_RANGE: // 超出二級索引范圍err = DB_RECORD_NOT_FOUND;goto idx_cond_failed;case ICP_MATCH: // 滿足二級索引條件,通過回表查找當(dāng)前記錄的事務(wù)id,以獲取當(dāng)前事務(wù)的上一版本數(shù)據(jù)goto requires_clust_rec;}ut_error; }}}// 省略...// 如果不是在主鍵上掃描,則判斷是否需要回表if (index != clust_index && prebuilt->need_to_access_clustered) { // MRR 的情況下 need_to_access_clustered = falserequires_clust_rec: // 主要用于非聚簇索引情況下的回表// 省略...// 使用二級索引對應(yīng)的主鍵進(jìn)行回表,拿到完整記錄后,會在該函數(shù)中調(diào)用lock_clust_rec_cons_read_sees函數(shù)再次判斷當(dāng)前記錄的可見性err = row_sel_get_clust_rec_for_mysql(prebuilt, index, rec, thr, &clust_rec, &offsets, &heap,need_vrow ? &vrow : nullptr, &mtr, prebuilt->get_lob_undo());// 省略.../*-------------------------------------------------------------*//* 階段5:將光標(biāo)移動到下一個索引記錄 */// 省略...
}
2)lock_sec_rec_cons_read_sees 函數(shù)
/** Checks that a non-clustered index record is seen in a consistent read. */
bool lock_sec_rec_cons_read_sees(const rec_t *rec, /*!< in: user record whichshould be read or passed overby a read cursor */const dict_index_t *index, /*!< in: index */const ReadView *view) /*!< in: consistent read view */
{ut_ad(page_rec_is_user_rec(rec)); /* NOTE that we might call this function while holding the searchsystem latch. */// 當(dāng)crash時,使用redo log恢復(fù)記錄期間為falseif (recv_recovery_is_on()) {return (false);// 臨時表的讀取始終是一致的,因為臨時表在多個連接和事務(wù)中不是共享的,所以讀取總是具有一致性} else if (index->table->is_temporary()) { return (true);}// 從記錄所在頁獲取最大事務(wù)ID(是一種優(yōu)化,以減少回表)// 二級索引掃描 rec 中存儲的只是主鍵 id,如果不使用 page 的最大事務(wù) ID,可能需要進(jìn)行大量回表,以獲取當(dāng)前記錄的事務(wù) IDtrx_id_t max_trx_id = page_get_max_trx_id(page_align(rec)); ut_ad(max_trx_id > 0); // 確保最大的事務(wù)ID大于0// sees 函數(shù)通過判斷 max_trx_id 是否小于低水位線(m_up_limit_id),若小于,則可見return (view->sees(max_trx_id));
}
MVCC 是否解決了幻讀
MVCC 解決了部分幻讀,但并沒有完全解決幻讀。
對于快照讀,MVCC 因為從 ReadView 讀取,所以必然不會看到新插入的行,解決了幻讀問題。
對于當(dāng)前讀,MVCC 無法解決幻讀。需要使用 Gap Lock 或 Next-Key Lock(Gap Lock + Record Lock)來解決。
其實原理也很簡單,用上面的例子稍微修改下以觸發(fā)當(dāng)前讀:select * from user where id < 10 for update,當(dāng)使用了 Gap Lock 時,Gap 鎖會鎖住 id < 10 的整個范圍,因此其他事務(wù)無法插入 id < 10 的數(shù)據(jù),從而防止了幻讀。
Repeatable Read 如何解決幻讀
SQL 標(biāo)準(zhǔn)中規(guī)定的 RR 并不能消除幻讀,但是 MySQL InnoDB 的 RR 可以,靠的就是 Gap 鎖。在 RR 級別下,Gap 鎖是默認(rèn)開啟的,而在 RC 級別下,Gap 鎖是關(guān)閉的。
RR 與 RC 生成 ReadView 的時機(jī)與區(qū)別
假設(shè)有一張表,其結(jié)構(gòu)和數(shù)據(jù)如下:
mysql> show create table t;
+-------+------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------+
| t | CREATE TABLE `t` (`id` int NOT NULL,`a` int DEFAULT NULL,PRIMARY KEY (`id`),KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci |
+-------+-----------------------------------------------------------------+
1 row in set (0.00 sec)mysql> select * from t;
+----+------+
| id | a |
+----+------+
| 1 | 50 |
+----+------+
1 row in set (0.00 sec)
RR 或 RC 生成 ReadView 的時機(jī)
解析:RR 生成 ReadView 的時機(jī)是事務(wù)第一個 select 的時候,而不是事務(wù)開始的時候。右邊的例子中,事務(wù)1在事務(wù)2提交了修改后才執(zhí)行第一個 select,因此生成的 ReadView 中,a 的是 100 而不是事務(wù)1剛開始時的 50。
RR 與 RC 生成 ReadView 的區(qū)別
?解析:RR 只在事務(wù)第一次執(zhí)行 select 時生成 ReadView ,之后一直使用該 ReadView。而 RC 級別則在每次執(zhí)行 select 時,都會生成一個 ReadView,所以在第二次執(zhí)行 select 時,讀取到了事務(wù) 2 對于 a 的修改值。
如果需要本文 WORD、PDF 相關(guān)文檔請在評論區(qū)留言!!!?
如果需要本文 WORD、PDF 相關(guān)文檔請在評論區(qū)留言!!!?
如果需要本文 WORD、PDF 相關(guān)文檔請在評論區(qū)留言!!!?
參考
MVCC的底層原理_mysql mvcc 高水位低水位-CSDN博客
MVCC原理分析 + 源碼解讀 -- 必須說透_mvcc mysql源碼解析-CSDN博客
MySQL 8.0 MVCC 核心原理解析(核心源碼) - 知乎 (zhihu.com)
InnoDB MVCC 詳解-騰訊云開發(fā)者社區(qū)-騰訊云 (tencent.com)