首先說明下,這裏主要內容爲整理總結網絡搜索的零散信息。mysql
寫在最前面,mysql事務是在Innodb引擎中得以實現的,若是這點不瞭解的話,請自行了解。sql
事務直接數據的可見性經過MVCC(多版本併發控制)實現。對同一記錄的修改會保存歷史版本的數據,經過一系列的邏輯看判斷當前事務應該獲取的是那個版本的數據,也就是一般意義上的可見性。數據庫
Innodb會爲每行記錄添加三個隱形字段:6字節的事務ID(DB_TRX_ID)、7字節的回滾指針(DB_ROLL_PTR)、隱藏的ID。數組
MVCC 在mysql 中的實現依賴的是 undo log 與 read view。網絡
a.undo log: undo log中記錄的是數據表記錄行的多個版本,也就是事務執行過程當中的回滾段,其實就是MVCC 中的一行原始數據的多個版本鏡像數據。併發
b.read view: 主要用來判斷當前版本數據的可見性。性能
下面看下一條記錄的更新過程:ui
1.初始數據行
F1~F6是某行列的名字,1~6是其對應的數據。後面三個隱含字段分別對應該行的事務號和回滾指針,假如這條數據是剛INSERT的,能夠認爲ID爲1,其餘兩個字段爲空。
2.事務1更改該行的各字段的值
當事務1更改該行的值時,會進行以下操做:
用排他鎖鎖定該行
記錄redo log
把該行修改前的值Copy到undo log,即上圖中下面的行
修改當前行的值,填寫事務編號,使回滾指針指向undo log中的修改前的行
3.事務2修改該行的值
下面講下調用select時mysql到底作了什麼:spa
首先,看下read_view結構:.net
struct read_view_t{ // 因爲是逆序排列,因此low/up有所顛倒 // 能看到當前行版本的高水位標識,>= low_limit_id皆不能看見,low_limit_id取值爲max_trx_id(即還沒有被分配的trx_id) trx_id_t low_limit_id; // 能看到當前行版本的低水位標識,< up_limit_id皆能看見,up_limit_id取值爲當前活躍最小事務id trx_id_t up_limit_id; // 當前活躍事務(即未提交的事務)的數量 ulint n_trx_ids; // 以逆序排列的當前獲取活躍事務id的數組 // 其up_limit_id<tx_id<low_limit_id trx_id_t* trx_ids; // 建立當前視圖的事務id trx_id_t creator_trx_id; // 事務系統中的一致性視圖鏈表 UT_LIST_NODE_T(read_view_t) view_list; };
read_view構建邏輯:
在mysql的trx_sys中,一直維護着一個全局的活躍的讀寫事務id(trx_sys->descriptors
),id按照從小到大排序,表示在某個時間點,數據庫中全部的活躍(已經開始但還沒提交)的讀寫(必須是讀寫事務,只讀事務不包含在內)事務。當須要一個一致性讀的時候(即建立新的readview時),會把全局讀寫事務id拷貝一份到readview本地(read_view_t->trx_ids),當作當前事務的快照。read_view_t->up_limit_id是read_view_t->trx_ids這數組中最小的值,read_view_t->low_limit_id是建立readview時的max_trx_id(即還沒有被分配的trx_id,這樣在>=判斷時就能夠將讀事務開啓後提交的事務包含進來),即必定大於read_view_t->trx_ids中的最大值。當查詢出一條記錄後(記錄上有一個trx_id,表示這條記錄最後被修改時的事務id),可見性判斷的邏輯以下(read_view_sees_trx_id):
1.若是記錄上的trx_id小於read_view_t->up_limit_id,則說明這條記錄的最後修改在readview建立以前,所以這條記錄能夠被看見。
2.若是記錄上的trx_id大於等於read_view_t->low_limit_id,則說明這條記錄的最後修改在readview建立以後,所以這條記錄確定不能夠被看見。
3.若是記錄上的trx_id在up_limit_id和low_limit_id之間,且trx_id在read_view_t->trx_ids之中,則表示這條記錄的最後修改是在readview建立之時,被另一個活躍事務所修改,因此這條記錄也不能夠被看見。若是trx_id不在read_view_t->trx_ids之中,則表示這條記錄的最後修改在readview建立以後被提交,因此能夠看到。
注意當隔離級別設置爲READ UNCOMMITTED時,不會去構建老版本。
判斷行記錄可見行源碼以下:
/*********************************************************************//** 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, /*!< in: read view */ trx_id_t trx_id) /*!< in: trx id */ { if (trx_id < view->up_limit_id) { return(true); } else if (trx_id >= view->low_limit_id) { return(false); } else { ulint lower = 0; ulint upper = view->n_trx_ids - 1; ut_a(view->n_trx_ids > 0); do { ulint mid = (lower + upper) >> 1; trx_id_t mid_id = view->trx_ids[mid]; if (mid_id == trx_id) { return(FALSE); } else if (mid_id < trx_id) { if (mid > 0) { upper = mid - 1; } else { break; } } else { lower = mid + 1; } } while (lower <= upper); } return(true); }
4.基於上述判斷,若是記錄不可見,則嘗試使用undo去構建老的版本(row_vers_build_for_consistent_read
),直到找到能夠被看見的記錄或者解析完全部的undo,代碼以下:
dberr_t row_vers_build_for_consistent_read(...) { ...... for(;;){ err = trx_undo_prev_version_build(rec, mtr,version,index,*offsets, heap,&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; } ...... }
可見性分析如上已經差很少了,那麼,不一樣隔離級別是怎麼利用readview達到效果的呢?
針對RR隔離級別,在第一次建立readview(第一次調用select(不加鎖))後,這個readview就會一直持續到事務結束,也就是說在事務執行過程當中,數據的可見性不會變,因此在事務內部不會出現不一致的狀況。針對RC隔離級別,事務中的每一個查詢語句都單獨構建一個readview,因此若是兩個查詢之間有事務提交了,兩個查詢讀出來的結果就不同。從這裏能夠看出,在InnoDB中,RR隔離級別的效率是比RC隔離級別的高。此外,針對RU隔離級別,因爲不會去檢查可見性,因此在一條SQL中也會讀到不一致的數據。針對串行化隔離級別,InnoDB是經過鎖機制來實現的,而不是經過多版本控制的機制,因此性能不好。
由下面代碼可知,只有單純的select才建立readview,select for update會加鎖因此不會建立readview。
// 只有非鎖模式的select才建立一致性視圖 else if (prebuilt->select_lock_type == LOCK_NONE) { // 建立一致性視圖 trx_assign_read_view(trx); prebuilt->sql_stat_start = FALSE; }
也可參考下面描述:
參考資料:
https://my.oschina.net/alchemystar/blog/1927425
http://mysql.taobao.org/monthly/2017/12/01/