mvcc原理_MVCC原理探究及MySQL源码实现分析
沃趣科技數據庫專家 ?董紅禹
MVCC原理探究及MySQL源碼實現分析
數據庫多版本讀場景
session 1
session 2
select a from test; return a = 10
start transaction;
update test set a = 20;
start transaction;
select a from test; return ?
commit;
select a from test; return ?
我們看下上面這個數據庫日常操作的例子。
session 1修改了一條記錄,沒有提交;與此同時,session 2 來查詢這條記錄,這時候返回記錄應該是多少呢?
session 1 提交之后 session 2 查詢出來的又應該是多少呢?
由于MySQL支持多種隔離級別,這個問題是需要看session2的事務隔離級別的,情況如下:
隔離級別為 READ-UNCOMMITTED 情況下:
session 1 commit前后 session 2 去查看都會看到的是修改后的結果 a = 20
隔離級別為 READ-COMMITTED 情況下:
session 1 commit 前查看到的還是 a =10 , commit之后看到的是 a = 20
隔離級別為 REPEATABLE-READ, SERIALIZABLE 情況下:
session 1 commit前后 session 2 去查看都會看到的是修改后的結果 a = 10
其實不管隔離級別,我們也拋開數據庫中的ACID,我們思考一個問題:眾所周知,InnoDB的數據都是存儲在B-tree里面的,修改后的數據到底要不要存儲在實際的B-tree葉子節點,session2是怎么做到查詢出來的結果還是10,而不是20呢?
MVCC實現原理
上述現象在數據庫中大家經常看到,但是數據庫到底是怎么實現的,深究的人就不多了。
其實原理很簡單,數據庫就是通過UNDO和MVCC來實現的。
通過DB_ROLL_PT 回溯查找數據歷史版本
首先InnoDB每一行數據還有一個DB_ROLL_PT的回滾指針,用于指向該行修改前的上一個歷史版本
當插入的是一條新數據時,記錄上對應的回滾段指針為NULL
更新記錄時,原記錄將被放入到undo表空間中,并通過DB_ROLL_PT指向該記錄。session2查詢返回的未修改數據就是從這個undo中返回的。MySQL就是根據記錄上的回滾段指針及事務ID判斷記錄是否可見,如果不可見繼續按照DB_ROLL_PT繼續回溯查找。
通過read view判斷行記錄是否可見
具體的判斷流程如下:
RR隔離級別下,在每個事務開始的時候,會將當前系統中的所有的活躍事務拷貝到一個列表中(read view)
RC隔離級別下,在每個語句開始的時候,會將當前系統中的所有的活躍事務拷貝到一個列表中(read view)
并按照以下邏輯判斷事務的可見性。
MVCC解決了什么問題
MVCC使得數據庫讀不會對數據加鎖,select不會加鎖,提高了數據庫的并發處理能力
借助MVCC,數據庫可以實現RC,RR等隔離級別,用戶可以查看當前數據的前一個或者前幾個歷史版本。保證了ACID中的I-隔離性。
MySQL代碼分析
前面我們介紹了什么是MVCC,以及它解決了什么問題。
下面我們來看一下在MySQL源碼中,到底是怎么實現這個邏輯的。
InnoDB隱藏字段源碼分析
InnoDB表中會存有三個隱藏字段,這三個字段是mysql默認幫我們添加的。我們可以通過代碼中查看到:
dict_table_add_system_columns(
/*==========================*/
dict_table_t*table,/*!< in/out: table */
mem_heap_t*heap)/*!< in: temporary 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 system
columns.
Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned off
for these tables. */
dict_mem_table_add_col(table,heap,"DB_ROW_ID",DATA_SYS,
DATA_ROW_ID|DATA_NOT_NULL,
DATA_ROW_ID_LEN);
#if (DATA_ITT_N_SYS_COLS != 2)
#error"DATA_ITT_N_SYS_COLS != 2"
#endif
#if DATA_ROW_ID != 0
#error"DATA_ROW_ID != 0"
#endif
dict_mem_table_add_col(table,heap,"DB_TRX_ID",DATA_SYS,
DATA_TRX_ID|DATA_NOT_NULL,
DATA_TRX_ID_LEN);
#if DATA_TRX_ID != 1
#error"DATA_TRX_ID != 1"
#endif
if(!table->is_intrinsic()){
dict_mem_table_add_col(table,heap,"DB_ROLL_PTR",DATA_SYS,
DATA_ROLL_PTR|DATA_NOT_NULL,
DATA_ROLL_PTR_LEN);
#if DATA_ROLL_PTR != 2
#error"DATA_ROLL_PTR != 2"
#endif
/* This check reminds that if a new system column is added to
the program, it should be dealt with here */
#if DATA_N_SYS_COLS != 3
#error"DATA_N_SYS_COLS != 3"
#endif
}
}
DB_ROW_ID:如果表中沒有顯示定義主鍵或者沒有唯一索引則MySQL會自動創建一個6字節的row id存在記錄中
DB_TRX_ID:事務ID
DB_ROLL_PTR:回滾段指針
InnoDB判斷事務可見性源碼分析
mysql中并不是根據事務的事務ID進行比較判斷記錄是否可見,而是根據每一行記錄上的事務ID進行比較來判斷記錄是否可見。
我們可以通過實驗驗證 , 創建一張表里面插入一條記錄
dhy@10.16.70.190:330612:25:47[dhy]>select*fromdhytest;
+------+
|id|
+------+
|10|
+------+
1rowinset(7.99sec)
手工開啟一個事務 更新一條記錄 但是并不提交:
dhy@10.10.80.199:330615:28:24[dhy]>update dhytestsetid=20;
QueryOK,3rows affected(40.71sec)
Rowsmatched:3Changed:3Warnings:0
在另外一個會話執行查詢
dhy@10.16.70.190:330612:38:33[dhy]>select*fromdhytest;
這時我們可以跟蹤調試mysql 查看他是怎么判斷記錄的看見性,中間函數調用太多列舉最重要部分
這里需要介紹一個重要的類 ReadView,Read View是事務開啟時當前所有事務的一個集合,這個類中存儲了當前Read View中最大事務ID及最小事務ID
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_tm_low_limit_id;
/** The read should see all trx ids which are strictly
smaller (
low water mark". */
trx_id_tm_up_limit_id;
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_tm_creator_trx_id;
當我們執行上面的查詢語句時,跟蹤到主要函數如下:
函數row_search_mvcc->lock_clust_rec_cons_read_sees
bool
lock_clust_rec_cons_read_sees(
/*==========================*/
constrec_t*rec,/*!< in: user record which should be read or
passed over by a read cursor */
dict_index_t*index,/*!< in: clustered index */
constulint*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 multiple
transactions from different connections cannot simultaneously
operate on same temp-table and so read of temp-table is
always consistent read. */
//只讀事務或者臨時表是不需要一致性讀的判斷
if(srv_read_only_mode||index->table->is_temporary()){
ut_ad(view==0||index->table->is_temporary());
return(true);
}
/* NOTE that we call this function while holding the search
system latch. */
trx_id_ttrx_id=row_get_rec_trx_id(rec,index,offsets);//獲取記錄上的TRX_ID這里需要解釋下,我們一個查詢可能滿足的記錄數有多個。那我們每讀取一條記錄的時候就要根據這條記錄上的TRX_ID判斷這條記錄是否可見
return(view->changes_visible(trx_id,index->table->name));//判斷記錄可見性
}
下面是真正判斷記錄的看見性。
boolchanges_visible(
trx_id_tid,
consttable_name_t&name)const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id>0);
//如果ID小于Read View中最小的, 則這條記錄是可以看到。說明這條記錄是在select這個事務開始之前就結束的
if(id
return(true);
}
check_trx_id_sanity(id,name);
//如果比Read View中最大的還要大,則說明這條記錄是在事務開始之后進行修改的,所以此條記錄不應查看到
if(id>=m_low_limit_id){
return(false);
}elseif(m_ids.empty()){
return(true);
}
constids_t::value_type*p=m_ids.data();
return(!std::binary_search(p,p+m_ids.size(),id));//判斷是否在Read View中, 如果在說明在創建Read View時 此條記錄還處于活躍狀態則不應該查詢到,否則說明創建Read View是此條記錄已經是不活躍狀態則可以查詢到
}
對于不可見的記錄都是通過row_vers_build_for_consistent_read函數查詢UNDO構建老版本記錄,直到記錄可見。
這里需要說明一點 不同的事務隔離級別,可見性的實現也不一樣:
READ-COMMITTED
事務內的每個查詢語句都會重新創建Read View,這樣就會產生不可重復讀現象發生
REPEATABLE-READ
事務內開始時創建Read View , 在事務結束這段時間內 每一次查詢都不會重新重建Read View , 從而實現了可重復讀。
參考資料:
《唐成-2016PG大會-數據庫多版本實現內幕.pdf》
總結
以上是生活随笔為你收集整理的mvcc原理_MVCC原理探究及MySQL源码实现分析的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 一名老工程师的感言
- 下一篇: 吉他谱——有多少爱可以重来
