Mysql中有讀鎖和寫鎖,在沒有引入MVVC之前,讀鎖是允許共享讀,但是如果一行記錄事先被上了寫鎖,那么就不允許其他事務(wù)進(jìn)行讀,現(xiàn)在的大部分應(yīng)用都具有讀多寫少的特性,所以為了進(jìn)一步增加并發(fā)讀的性能,引入了MVVC-----Multi Version Concurrency Control(多版本并發(fā)控制)
從mysql-5.5.5開始,InnoDB作為默認(rèn)存儲(chǔ)引擎,InnoDB默認(rèn)隔離級(jí)別REPEATABLE READ, 行級(jí)鎖
在InnoDB中用B+樹作為索引的存儲(chǔ)結(jié)構(gòu),并且主鍵所在的索引為ClusterIndex(聚簇索引), ClusterIndex中的葉子節(jié)點(diǎn)中保存了對(duì)應(yīng)的數(shù)據(jù)內(nèi)容。一個(gè)表只能有一個(gè)主鍵,所以只能有一個(gè)聚簇索引,如果表沒有定義主鍵,則選擇第一個(gè)非NULL唯一索引作為聚簇索引,如果還沒有則生成一個(gè)隱藏id列作為聚簇索引。
除了Cluster Index外的索引是Secondary Index(輔助索引)。輔助索引中的葉子節(jié)點(diǎn)保存的是聚簇索引的葉子節(jié)點(diǎn)的值。
由于索引的組織方式為B+樹,在最底層的葉子節(jié)點(diǎn)層,數(shù)據(jù)頁之間相當(dāng)于是一個(gè)雙向鏈表,在插入的過程中,數(shù)據(jù)頁之間會(huì)引起裂變(相關(guān)信息可以參考 http://hedengcheng.com/?p=525)
無論是聚簇索引,還是二級(jí)索引,其每條記錄都包含了一個(gè)DELETED BIT位,用于標(biāo)識(shí)該記錄是否是刪除記錄。
InnoDB中數(shù)據(jù)行的組織格式大致為
在InnoDB中,每一行都有2個(gè)隱藏列DATA_TRX_ID和DATA_ROLL_PTR(如果沒有定義主鍵,則還有個(gè)隱藏主鍵列ROWID):
DATA_TRX_ID: 表示最近修改的事務(wù)的id
DATA_ ROLL_PTR: 表示指向該行回滾段(undo segment 中的 undo log)的指針,該行上所有舊的版本,在undo中都通過鏈表的形式組織,而該值,正式指向undo中該行的歷史記錄鏈表
事務(wù)鏈表
MySQL中的事務(wù)在開始到提交這段過程中,都會(huì)被保存到一個(gè)叫trx_sys的事務(wù)鏈表中,這是一個(gè)基本的鏈表結(jié)構(gòu):
事務(wù)鏈表中保存的都是還未提交的事務(wù),事務(wù)一旦被提交,則會(huì)被從事務(wù)鏈表中摘除。
ReadView
在MVVC的源碼實(shí)現(xiàn)中,一個(gè)比較重要的部分就是ReadView,介紹一下這個(gè)類,看一下源代碼
// Friend declaration
class MVCC;
/** Read view lists the trx ids of those transactions for which a consistent
read should not see the modifications to the database. */
...
class ReadView {
...
private:
// Prevent copying
ids_t(const ids_t&);
ids_t& operator=(const ids_t&);
private:
/** Memory for the array */
value_type* m_ptr;
/** Number of active elements in the array */
ulint m_size;
/** Size of m_ptr in elements */
ulint m_reserved;
friend class ReadView;
};
public:
ReadView();
~ReadView();
/** Check whether transaction id is valid.
@param[in] id transaction id to check
@param[in] name table name */
static void check_trx_id_sanity(
trx_id_t id,
const table_name_t& name);
// 判斷一個(gè)修改是否可見
/** 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. */
bool changes_visible(
trx_id_t id,
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
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));
}
private:
// Disable copying
ReadView(const ReadView&);
ReadView& operator=(const ReadView&);
private:
// 活動(dòng)事務(wù)中的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;
// 活動(dòng)事務(wù)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;
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;
/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
trx_id_t m_low_limit_no;
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed;
typedef UT_LIST_NODE_T(ReadView) node_t;
/** List of read views in trx_sys */
byte pad1[64 - sizeof(node_t)];
node_t m_view_list;
};
在一個(gè)事務(wù)中,處理可見性時(shí),主要用到的數(shù)據(jù)結(jié)構(gòu)如下
private:
// 活動(dòng)事務(wù)中的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;
// 活動(dòng)事務(wù)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;
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;
m_low_limit_id: 表示在當(dāng)前事務(wù)啟動(dòng)后,當(dāng)前的事務(wù)鏈表中,最大的事務(wù)id號(hào),也就是最近創(chuàng)建的除自身以外的最大事務(wù)id號(hào)
m_up_limit_id: 表示在當(dāng)前事務(wù)啟動(dòng)后,當(dāng)前的事務(wù)鏈表中,最小的事務(wù)id號(hào),也就是最近創(chuàng)建的最古老的還沒有提交的事務(wù)id號(hào)
m_creator_trx_id: 創(chuàng)建當(dāng)前事務(wù)的 trx_id (DATA_TRX_ID )
m_ids: 當(dāng)前這個(gè)讀快照中,事務(wù)鏈表中的全部事務(wù)數(shù)
如圖所示
根據(jù)這些屬性來判斷事務(wù)的可見性,先看代碼中如何處理:
// 判斷一個(gè)修改是否可見
/** 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. */
bool changes_visible(
trx_id_t id,
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
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));
}
很多方法的意思我也不知道,就看一下那幾個(gè)if else吧 首先
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
如果這個(gè)事務(wù)比事務(wù)鏈中最古老的事務(wù)版本號(hào)還要早,那么它肯定是在我們當(dāng)前事務(wù)開啟之前已經(jīng)完成了提交,是可以看見的
又或者這個(gè)事務(wù)就是在我們當(dāng)前事務(wù)中進(jìn)行開啟的(id == m_creator_trx_id),那么這個(gè)事務(wù)所做的改變我們也可以看見
再接著看
if (id >= m_low_limit_id) {
return(false);
} else if (m_ids.empty()) {
return(true);
}
InnoDB默認(rèn)的是RR級(jí)別,在這種級(jí)別下,相當(dāng)于事務(wù)開啟后,事務(wù)鏈中所有的事務(wù),它們在事務(wù)處理期間的一切改變對(duì)我們當(dāng)前開啟的事務(wù)而言都是不可見的,也可以相當(dāng)于看作 m_up_limit_id == m_low_limit_id 。
如果事務(wù)鏈中是空的,也就是所有的事務(wù)都是可見的
在這里,可見包括兩層含義:
記錄可見,且Deleted bit = 0;當(dāng)前記錄是可見的有效記錄。
記錄可見,且Deleted bit = 1;當(dāng)前記錄是可見的刪除記錄。此記錄在本事務(wù)開始之前,已經(jīng)刪除。
使用主鍵(聚簇索引)查找時(shí),當(dāng)發(fā)現(xiàn)事務(wù)不可見的時(shí)候,可以根據(jù)DATA_ROLL_PTR進(jìn)行回滾,查看上一個(gè)事務(wù)記錄中的數(shù)據(jù)是否可見。
非主鍵(二級(jí)索引)查找時(shí),流程有一些不同:
首先,查看二級(jí)索引頁面的最大更新事務(wù)MAX_TRX_ID號(hào),如果MAX_TRX_ID < m_up_limit_id,當(dāng)前頁面所有數(shù)據(jù)均可見,本頁面可以進(jìn)行索引覆蓋性掃描。丟棄所有deleted bit = 1的記錄,返回deleted bit = 0 的記錄
如果不能滿足MAX_TRX_ID < m_up_limit_id,說明當(dāng)前頁面無法進(jìn)行索引覆蓋性掃描,此時(shí)需要針對(duì)每一項(xiàng),到聚簇索引中判斷可見性。這時(shí)候就可能會(huì)出現(xiàn),在二級(jí)索引頁面中,有多個(gè)符合查找條件的二級(jí)索引記錄項(xiàng),它們指向了聚簇索引界面的同一個(gè)記錄,那么如何避免返回多次相同的聚簇索引記錄呢? 代碼如下
if (clust_rec
&& (old_vers || rec_get_deleted_flag(rec,dict_table_is_comp(sec_index->table)))
&& !row_sel_sec_rec_is_for_clust_rec(rec, sec_index, clust_rec, clust_index))
滿足以上if判斷的所有聚簇索引記錄,都直接丟棄,以上判斷的邏輯如下:
1.需要回聚簇索引掃描,并且獲得記錄
2.聚簇索引記錄為回滾版本,或者二級(jí)索引中的記錄為刪除版本
3.聚簇索引項(xiàng),與二級(jí)索引項(xiàng),其鍵值并不相等
注意 一定要1.2.3這三個(gè)條件同時(shí)滿足才會(huì)被丟棄
講完了可見性,再更深入一些看一下這整個(gè)過程,MVVC是如何進(jìn)行操作的(以下內(nèi)容均參考何登成大大博客):
在更新操作中,更新前后的數(shù)據(jù)行在聚簇索引中存在了兩條記錄,區(qū)別在于,舊數(shù)據(jù)的Deleted設(shè)為1,同時(shí) DATA_ ROLL_PTR指向Undo segment中之前的版本
對(duì)于聚簇索引,如果更新操作沒有更新primary key,那么更新不會(huì)產(chǎn)生新的記錄項(xiàng),而是在原有記錄上進(jìn)行更新,老版本進(jìn)入undo表空間,通過記錄上的undo指針進(jìn)行回滾。同時(shí)DATA_TRX_ID進(jìn)行了更新
對(duì)于二級(jí)索引,如果更新操作沒有更新其鍵值,那么二級(jí)索引記錄保持不變。
對(duì)于二級(jí)索引,更新操作無論更新primary key,或者是二級(jí)索引鍵值,都會(huì)導(dǎo)致二級(jí)索引產(chǎn)生新版本數(shù)據(jù)(新的數(shù)據(jù)記錄)。
聚簇索引設(shè)置記錄deleted bit時(shí),會(huì)同時(shí)更新DATA_TRX_ID列。老版本DATA_TRX_ID進(jìn)入undo表空間;二級(jí)索引設(shè)置deleted bit時(shí),不寫入undo。
Purge操作
對(duì)于用戶刪除的數(shù)據(jù),InnoDB并不是立刻刪除,而是標(biāo)記一下,后臺(tái)線程批量的真正刪除。這個(gè)線程就是后臺(tái)的Purge線程。此外,過期的undolog也需要回收,這里說的過期,指的是undo不需要被用來構(gòu)建之前的版本,也不需要用來回滾事務(wù)。
關(guān)于Purge流程,可以參考http://mysql.taobao.org/monthly/2018/03/01/
參考:
http://hedengcheng.com/?p=148
https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/
http://www.ywnds.com/?p=10418