1. 程式人生 > >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進行比較來判斷記錄是否可見。

我們可以通過實驗驗證 , 建立一張表裡面插入一條記錄:

[email protected]:3306  12:25:47 [dhy]>select * from dhytest;
+------+
| id   |
+------+
|   10 |
+------+
1 row in set (7.99 sec)

手工開啟一個事務 更新一條記錄 但是並不提交:

dhy@10.10.80.199:3306  15:28:24 [dhy]>update dhytest set id = 20;
Query OK, 3 rows affected (40.71 sec)
Rows matched: 3  Changed: 3  Warnings: 0

在另外一個會話執行查詢:

[email protected]10.16.70.190:3306  12:38:33 [dhy]>select * from dhytest;

這時我們可以跟蹤除錯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_t    m_low_limit_id;
/** The read should see all trx ids which are strictly
smaller (<) than this value.  In other words, this is the
low water mark". */
trx_id_t    m_up_limit_id;
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t    m_creator_trx_id;

當我們執行上面的查詢語句時,跟蹤到主要函式如下:

函式row_search_mvcc->lock_clust_rec_cons_read_sees
bool
lock_clust_rec_cons_read_sees(
/*==========================*/
const rec_t*    rec,    /*!< in: user record which should be read or
passed 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 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_t    trx_id = row_get_rec_trx_id(rec, index, offsets); //獲取記錄上的TRX_ID這裡需要解釋下,我們一個查詢可能滿足的記錄數有多個。那我們每讀取一條記錄的時候就要根據這條記錄上的TRX_ID判斷這條記錄是否可見
return(view->changes_visible(trx_id, index->table->name)); //判斷記錄可見性
}

下面是真正判斷記錄的看見性。

bool changes_visible(
trx_id_t    id,
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
//如果ID小於Read View中最小的, 則這條記錄是可以看到。說明這條記錄是在select這個事務開始之前就結束的
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
//如果比Read View中最大的還要大,則說明這條記錄是在事務開始之後進行修改的,所以此條記錄不應檢視到
if (id >= m_low_limit_id) {
return(false);
} else if (m_ids.empty()) {
return(true);
}
const ids_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》

來源:沃趣科技(woqutech)
作者:董紅禹

相關推薦

MVCC原理探究MySQL原始碼實現分析

資料庫多版本讀場景session 1 session 2 select a from test; return a = 10 start transaction; update test set a = 20; start trans

【java併發程式設計】執行緒池原理分析ThreadPoolExecutor原始碼實現

執行緒池簡介:  多執行緒技術主要解決處理器單元內多個執行緒執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。         假設一個伺服器完成一項任務所需時間為:T1 建立執行緒時間,T2 線上程中執行任務的時間,T3 銷燬執行緒時間。    

B+樹原理mysql的索引分析

B+/-Tree原理 B-Tree介紹 B-Tree是一種多路搜尋樹(並不是二叉的):       1.定義任意非葉子結點最多隻有M個兒子;且M>2;       2.根結點的兒子數為[2, M];       3.除根結點以外的非葉子結點的兒子數為[M/2, M

B+/-Tree原理mysql的索引分析

B+/-Tree原理 B-Tree介紹 B-Tree是一種多路搜尋樹(並不是二叉的):       1.定義任意非葉子結點最多隻有M個兒子;且M>2;       2.根結點的兒子數為[2, M];       3.除根結點以外的非葉子結點的兒子數為[M/2, M];

ArrayList工作原理底層原始碼實現

一.概述 以陣列實現。節約空間,但陣列有容量限制。超出限制時會增加50%容量,用System.arraycopy()複製到新的陣列。因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。 按陣列下標訪問元素-get(i)、set(i,e) 的效能很高,這是陣列的基本優勢。

Promise原理探究實現

前言 作為ES6處理非同步操作的新規範,Promise一經出現就廣受歡迎。面試中也是如此,當然此時對前端的要求就不僅僅侷限會用這個階段了。下面就一起看下Promise相關的內容。 Promise用法及實現 在開始之前,還是簡單回顧下Promise是什麼以及怎麼用,直接上來談實現有點空中花園的感覺。(下面示例參

MySQL MGR實現分析 - 成員管理與故障恢復實現

www 進程 mes The rapi ember chang ava 網易雲 此文已由作者溫正湖授權網易雲社區發布。歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。MySQL Group Replication(MGR)框架讓MySQL具備了自動主從切換和故障恢復能力,

【劉文彬】【精解】EOS標準貨幣體系與原始碼實現分析

原文連結:醒者呆的部落格園,https://www.cnblogs.com/Evsward/p/eos-exchange.html EOS智慧合約中包含一個exchange合約,它支援使用者建立一筆交易,是任何兩個基本貨幣型別之間的交易。這個合約的作用是跨不同幣種(都是EOS上的

併發實戰——原子類AtomicReference底層原始碼CompareAndSwapObject分析

本文內容: 分析原子類AtomicReference 分析原始碼 AtomicReference中 public final boolean compareAndSet(V expect, V update) { return

霍夫變換直線檢測houghlinesopencv的實現分析

導讀: 1. houghlines的演算法思想 2. houghlines實現需要考慮的要素 3. houghlines的opencv實現,程式碼分析 4. houghlines的效率分析,改進 1. houghlines的演算法思想 檢測直線,houghlines標準演算

SVM基礎java原始碼實現

當權重向量是單位向量時,幾何間隔就是函式間隔! 【1】最速下降法(或:梯度法) 最速下降法(梯度下降法)只用到了梯度資訊,即目標函式的一階導數資訊, 【2】牛頓法 而牛頓法則同時用到了一階導數和二階導數(Hesse矩陣(二階導數矩陣))資訊。在star

分水嶺演算法分割影象的原理概述OpenCV程式碼實現

前面博文中提到的影象閾值化,影象邊緣檢測,影象輪廓檢測實際上都是一種影象分割法,影象分割在影象處理識別是非常重要的,這點沒有做過影象識別或影象處理的人都很容易想到,就不多解釋了。所以影象分割法就根據不同的應用需要出現了很多方法。 本篇博文介紹利用分水嶺進行影象分割的方法。它

影象特徵檢測描述(一):SIFT、SURF、ORB、HOG、LBP特徵的原理概述OpenCV程式碼實現

什麼叫特徵檢測?就是檢測影象中目標的特徵唄,所謂特徵,不管你怎麼旋轉目標,離目標遠近,它的特徵都應不變才對,這兩個特性稱為叫旋轉不變性和尺度不變性。當然還有其它特徵,如光照不一樣,也不應該變化嘛,只是旋轉不變性和尺度不變性是最基本的兩個要求。 對特徵的描述有很多種方法和運算

資料庫分散式事務XA規範介紹Mysql底層實現機制【原創】

1. 引言 分散式事務主要應用領域主要體現在資料庫領域、微服務應用領域。微服務應用領域一般是柔性事務,不完全滿足ACID特性,特別是I隔離性,比如說saga不滿足隔離性,主要是通過根據分支事務執行成功或失敗,執行相應的前滾的重試或者後滾的補償操作來達成全域性事務的最終一致性,但是全域性事務與全域性事務之間沒有

Android中三級快取實現原理LruCache 原始碼分析

介紹 oom異常:大圖片導致 圖片的三級快取:記憶體、磁碟、網路 下面通過一張圖來了解下三級快取原理: 程式碼: public class Davince { //使用固定執行緒池優化 private static Exec

Android XListView實現原理講解分析

就是 指定 不同 true -h -name 修改 一個 部分 XListview是一個非常受歡迎的下拉刷新控件,但是已經停止維護了。之前寫過一篇XListview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下XListview的實現原理,學到了很多,今天分享給大家。

區塊鏈教程Fabric1.0原始碼分析Peer peer channel命令子命令實現

  區塊鏈教程Fabric1.0原始碼分析Peer peer channel命令及子命令實現,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric1.0原始碼筆記之P

MysqlMVCC的使用原理詳解

準備 測試環境:Mysql 5.7.20-log 資料庫預設隔離級別:RR(Repeatable Read,可重複讀),MVCC主要適用於Mysql的RC,RR隔離級別 建立一張儲存引擎為testmvcc的表,sql為: CREATE TABLE testmvcc ( id int

MySQL 原始碼分析 Innodb緩衝池刷髒的多執行緒實現

簡介 為了提高效能,大多數的資料庫在操作資料時都不會直接讀寫磁碟,而是中間經過緩衝池,將要寫入磁碟的資料先寫入到緩衝池裡,然後在某個時刻後臺執行緒把修改的資料刷寫到磁碟上。MySQL的InnoDB引擎也使用緩衝池來快取從磁碟讀取或修改的資料頁,如果當前資料庫需要操作的資料集比緩衝池中的空閒頁面大

HashMap原始碼實現原理底層結構

Java為資料結構中的對映定義了一個介面java.util.Map,此介面主要有四個常用的實現類,分別是HashMap、Hashtable、LinkedHashMap和TreeMap。 HashMap:HashMap是陣列+連結串列實現的,它根據鍵的hashCode值儲存資料,大多數情況下可