MySQL的MVCC原理

一 序

上一篇介紹了《事務隔離》,本文繼續整理MVCC實現原理。

二 鎖

讀鎖:也叫共享鎖、S鎖,若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S 鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

寫鎖:又稱排他鎖、X鎖。若事務T對數據對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。

表鎖:操作對象是數據表。Mysql大多數鎖策略都支持(常見mysql innodb),是系統開銷最低但併發性最低的一個鎖策略。事務t對整個表加讀鎖,則其他事務可讀不可寫,若加寫鎖,則其他事務增刪改都不行。

行級鎖:操作對象是數據表中的一行。是MVCC技術用的比較多的,但在MYISAM用不了,行級鎖用mysql的儲存引擎實現而不是mysql服務器。但行級鎖對系統開銷較大,處理高併發較好。

三 MVCC

3.1 原理

《高性能MySQL》第三版介紹這裏1.4節:

        可以認爲MVCC是行鎖的一個變種,大都實現了非阻塞的讀操作,寫操作也只鎖定了必要的行。

        MVCC的實現,是通過保存數據在某個時間點的快照來實現的,根據事務開始時間的不同,每個事務對於同一張表,同一時刻看到的數據可能是不同的。

上面是大概介紹,對於具體實現缺比較模糊,下面是原理介紹。

        InnoDB的 MVCC ,是通過在每行記錄的後面保存兩個隱藏的列來實現的。這兩個列, 一個保存了行的創建時間,一個保存了行的過期時間(或刪除時間), 當然存儲的並不是實際的時間值,而是系統版本號。

網上搜了下,說是3個字段的多一些。看下官網的介紹:

InnoDB is a multi-versioned storage engine: it keeps information about old versions of changed rows, to support transactional features such as concurrency and rollback. This information is stored in the tablespace in a data structure called a rollback segment(after an analogous data structure in Oracle). InnoDB uses the information in the rollback segment to perform the undo operations needed in a transaction rollback. It also uses the information to build earlier versions of a row for a consistent read.

Internally, InnoDB adds three fields to each row stored in the database. A 6-byte DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row. Also, a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted. Each row also contains a 7-byte DB_ROLL_PTR field called the roll pointer. The roll pointer points to an undo log record written to the rollback segment. If the row was updated, the undo log record contains the information necessary to rebuild the content of the row before it was updated. A 6-byte DB_ROW_ID field contains a row ID that increases monotonically as new rows are inserted. If InnoDB generates a clustered index automatically, the index contains row ID values. Otherwise, the DB_ROW_IDcolumn does not appear in any index.

第一段介紹innodb是支持mvcc的引擎。靠的就是undo日誌的rollback segment。

下面介紹了是三個字段:

6字節的事務ID(DB_TRX_ID)   表示最後一個事務的更新和插入,(每處理一個事務,其值自動+1)

7字節的回滾指針(DB_ROLL_PTR)指向當前記錄項的rollback segment的undo log記錄,找之前版本的數據就是通過這個指針

6字節的DB_ROW_ID 標識插入的新的數據行的id

當然還有個刪除位,DELETE BIT位用於標識該記錄是否被刪除,這裏的不是真正的刪除數據,而是標誌出來的刪除。真正意義的刪除是在commit的時候。

具體源碼在dict_table_add_system_columns。

MVCC 在mysql 中的實現依賴的是 undo log 與 read view。

3.2 undo

undo log是爲回滾而用,具體內容就是copy事務前的數據庫內容(行)到undo buffer,在適合的時間把undo buffer中的內容刷新到磁盤.

行的更新過程

1 初始數據行

  

F1~F6是某行列的名字,1~6是其對應的數據。後面三個隱含字段分別對應該行的事務號和回滾指針,假如這條數據是剛INSERT的,可以認爲ID爲1,其他兩個字段爲空。

2. 事務1更改該行的各字段的值


當事務1更改該行的值時,會進行如下操作:
用排他鎖鎖定該行
記錄redo log
把該行修改前的值Copy到undo log,即上圖中下面的行

修改當前行的值,填寫事務編號,使回滾指針指向undo log中的修改前的行。

3 事務2修改改行的值


與事務1相同,此時undo log,中有有兩行記錄,並且通過回滾指針連在一起。
因此,如果undo log一直不刪除,則會通過當前記錄的回滾指針回溯到該行創建時的初始內容,所幸的時在Innodb中存在purge線程,它會查詢那些比現在最老的活動事務還早的undo log,並刪除它們,從而保證undo log文件不至於無限增長。

上述過程確切地說是描述了UPDATE的事務過程,其實undo log分insert和update undo log,因爲insert時,原始的數據並不存在,所以回滾時把insert undo log丟棄即可,而update undo log則必須遵守上述過程。

當然也有人順序有所調整,主要的沒變,就是有行鎖。


3.3 事務鏈表

MySQL中的事務在開始到提交這段過程中,都會被保存到一個叫trx_sys的事務鏈表中,這是一個基本的鏈表結構:


事務鏈表中保存的都是還未提交的事務,事務一旦被提交,則會被從事務鏈表中摘除。

RR隔離級別下,在每個事務開始的時候,會將當前系統中的所有的活躍事務拷貝到一個列表中(read view) 
RC隔離級別下,在每個語句開始的時候,會將當前系統中的所有的活躍事務拷貝到一個列表中(read view) 

這裏在客戶端執行命令:show engine innodb status

就能看到事務的list.

3.4 readview

  read view是幹什麼的呢?上面可知MVCC實現了多個併發事務更新同一行記錄會時產生多個記錄版本,那問題來了,新開始的事務如果要查詢這行記錄,應該獲取到哪個版本呢?就需要read view來解決行的可見性問題。

 Read View是事務開啓時當前所有事務的一個集合,這個類中存儲了當前Read View中最大事務ID及最小事務ID。祥見read_view_struct這個結構體

/** 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;
low_limit_id  前者表示事務id大於此值的行記錄都不可見

up_limit_id   後者表示事務id小於此值的行記錄都可見.


根據上圖所示,所有數據行上DATA_TRX_ID小於up_trx_id的記錄,說明修改該行的事務在當前事務開啓之前都已經提交完成,所以對當前事務來說,都是可見的。而對於DATA_TRX_ID大於low_trx_id的記錄,說明修改該行記錄的事務在當前事務之後,所以對於當前事務來說是不可見的。至於位於(up_trx_id, low_trx_id)中間的事務是否可見,這個需要根據不同的事務隔離級別來確定。對於RC的事務隔離級別來說,對於事務執行過程中,已經提交的事務的數據,對當前事務是可見的,也就是說上述圖中,當前事務運行過程中,trx1~4中任意一個事務提交,對當前事務來說都是可見的;而對於RR隔離級別來說,事務啓動時,已經開始的事務鏈表中的事務的所有修改都是不可見的,所以在RR級別下,low_trx_id基本保持與up_trx_id相同的值即可。

沒有自己讀innodb源碼,貼一下別人的。

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是此條記錄已經是不活躍狀態則可以查詢到
}

不同隔離級別ReadView實現方式

1. read-commited:

  函數:ha_innobase::external_lock

  if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
    && trx->global_read_view) {
    / At low transaction isolation levels we let
    each consistent read set its own snapshot /
  read_view_close_for_mysql(trx);

即:在每次語句執行的過程中,都關閉read_view, 重新在row_search_for_mysql函數中創建當前的一份read_view。

這樣就會產生不可重複讀現象發生。

2. repeatable read:

  在repeatable read的隔離級別下,創建事務trx結構的時候,就生成了當前的global read view。使用trx_assign_read_view函數創建,一直維持到事務結束。

  在事務結束這段時間內 每一次查詢都不會重新重建Read View , 從而實現了可重複讀。

要是覺得不好理解,看看別人整理的圖吧。


四 總結:

  相信理解了上面的MVCC的原理,再來看書上1.4後面介紹MVCC的應用就會好多了。

   SELECT
  InnoDB檢查每行,要確定它符合兩個標準。
  InnoDB必須知道行的版本號,這個行的版本號至少要和事物版本號一樣的老。(也就是是說它的版本號可能少於或者和事物版本號相同)。這個既能確定事物開始之前行是存在的,也能確定事物創建或修改了這行。
  行的刪除操作的版本一定是未定義的或者大於事物的版本號。確定了事物開始之前,行沒有被刪除。
  符合了以上兩點。會返回查詢結果。
  INSERT
  InnoDB記錄了當前新增行的系統版本號。
  DELETE
  InnoDB記錄的刪除行的系統版本號作爲行的刪除ID。
  UPDATE
  InnoDB複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號作爲了刪除行的版本。
    所有其他記錄的結果保存是,從未獲得鎖的查詢。這樣它們查詢的數據就會儘可能的快。要確定查詢行要遵循這些標準。缺點是存儲引擎要爲每一行存儲更多的數據,檢查行的時候要做更多的處理以及其他內部的一些操作。
    MVCC只能在可重複讀和可提交讀的隔離級別下生效。不可提交讀不能使用它的原因是不能讀取符合事物版本的行版本。它們總是讀取最新的行版本。可序列化不能使用MVCC的原因是,它總是要鎖定行。

看完這些與我之前印象中MVCC還是有較大區別的,以前一直以爲不加鎖實現了MVCC。就是每行都有版本號,保存時根據版本號決定是否成功,而innodb:

事務以排他鎖的形式修改原始數據
把修改前的數據存放於undo log,通過回滾指針與主數據關聯

修改成功(commit)啥都不做,失敗則恢復undo log中的數據(rollback)

  雖然不是真正意義的MVCC,MVCC對MySQL還有有作用的:

 MVCC使得數據庫讀不會對數據加鎖,select不會加鎖,提高了數據庫的併發處理能力;
藉助MVCC,數據庫可以實現RC,RR等隔離級別,用戶可以查看當前數據的前一個或者前幾個歷史版本,保證了ACID中的I-隔離性。

還有很多不懂的,繼續。。。。

參考:

https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html

https://blog.csdn.net/chen77716/article/details/6742128

https://blog.csdn.net/joy0921/article/details/80128857

http://www.ywnds.com/?p=10418

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章