MySQL 核心模塊揭秘
1. 引言
前面三篇文章,我們分別介紹了 InnoDB 表鎖、行鎖,以及它們的鎖結(jié)構(gòu)。
表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)是鎖模塊的基礎(chǔ)組成部分,它們就像一塊磚,哪里需要哪里搬。
然而,要蓋房子,光有磚不行,還得有鋼筋、水泥等材料,這些材料就由鎖模塊結(jié)構(gòu)提供。
鎖模塊結(jié)構(gòu)只有一個(gè)對(duì)象(lock_sys),在 InnoDB 中是全局唯一的。
2. 鎖模塊結(jié)構(gòu)
鎖模塊結(jié)構(gòu)類型為 lock_sys_t,去掉注釋以及兩個(gè)無關(guān)緊要的屬性之后,簡(jiǎn)化如下:
struct lock_sys_t {
locksys::Latches latches;
hash_table_t *rec_hash;
hash_table_t *prdt_hash;
hash_table_t *prdt_page_hash;
Lock_mutex wait_mutex;
srv_slot_t *waiting_threads;
srv_slot_t *last_slot;
bool rollback_complete;
std::chrono::steady_clock::duration n_lock_max_wait_time;
os_event_t timeout_event;
}
單從屬性數(shù)量上看,鎖模塊結(jié)構(gòu)并不復(fù)雜,甚至可以說比較簡(jiǎn)單。
其實(shí),鎖模塊的復(fù)雜性,不在于表鎖結(jié)構(gòu)、行鎖結(jié)構(gòu),也不在于鎖模塊結(jié)構(gòu),而是在于各個(gè)事務(wù)、各種加鎖場(chǎng)景相互交錯(cuò)導(dǎo)致的錯(cuò)綜復(fù)雜的加鎖結(jié)果。
例如,一個(gè)事務(wù)等待獲得另一個(gè)事務(wù)持有的鎖,雖然會(huì)出現(xiàn)或長(zhǎng)或短的等待鏈,但也不算太壞的情況。更壞的情況是出現(xiàn)了環(huán)形的等待鏈,也就是出現(xiàn)了死鎖。
如果出現(xiàn)死鎖,我們又需要被動(dòng)復(fù)現(xiàn)死鎖,以解釋形成死鎖的原因,那簡(jiǎn)直頭大了。
為了不滑入復(fù)雜的深淵,我們就此打住,先來介紹鎖模塊結(jié)構(gòu)的屬性。
鎖模塊結(jié)構(gòu)中有三個(gè)類型為 hash_table_t 的屬性,分別是 rec_hash、prdt_hash、prdt_page_hash。
其中,prdt_hash、prdt_page_hash 由謂詞鎖使用。我們并不打算介紹謂詞鎖,忽略這兩個(gè)屬性,也就順理成章了。
n_lock_max_wait_time 屬性的值是 MySQL 本次啟動(dòng)以來,行鎖的最長(zhǎng)等待時(shí)間。通過以下命令可以查詢到這個(gè)屬性的值:
show status like 'innodb_row_lock_time_max';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| Innodb_row_lock_time_max | 50157 |
+--------------------------+-------+
rollback_complete 屬性,用于 MySQL 啟動(dòng)過程中,標(biāo)識(shí)從 undo 日志中恢復(fù)出來的、需要回滾的事務(wù)是否已全部回滾完成。
如果 rollback_complete = false,說明從 undo 日志中恢復(fù)出來的、需要回滾的事務(wù)還沒有全部回滾完成,InnoDB 會(huì)遍歷讀寫事務(wù)鏈表(trx_sys->rw_trx_list),釋放這些事務(wù)加的表鎖和行鎖。
這些事務(wù)全部回滾完成之后,rollback_complete 會(huì)被修改為 true。
前面介紹了鎖模塊結(jié)構(gòu)中兩個(gè)比較簡(jiǎn)單的屬性,剩下的其它屬性,我們分為幾個(gè)小節(jié)一一介紹。
2.1 誰來管理行鎖結(jié)構(gòu)?
上一篇文章,我們介紹過,事務(wù)對(duì)多條記錄加行鎖,滿足條件時(shí),可以共用一個(gè)行鎖結(jié)構(gòu)。
雖然共用能減少行鎖結(jié)構(gòu)的數(shù)量,但是,同一時(shí)刻,InnoDB 中可能還是有很多行鎖結(jié)構(gòu)。
這么多行鎖結(jié)構(gòu),要怎么組織,用到時(shí)才能方便、快速的找到呢?
這就需要用到鎖模塊結(jié)構(gòu)的 rec_hash 屬性了。
rec_hash 屬性是個(gè)哈希表,它的類型為 hash_table_t,創(chuàng)建鎖模塊對(duì)象(lock_sys)之后分配內(nèi)存:
void lock_sys_create(ulint n_cells)
{
...
// 創(chuàng)建鎖模塊對(duì)象,分配內(nèi)存
lock_sys = static_cast<lock_sys_t *>(
ut::zalloc_withkey(...));
...
// 創(chuàng)建哈希表(rec_hash),分配內(nèi)存
lock_sys->rec_hash =
ut::new_<hash_table_t>(n_cells);
...
}
lock_sys_create() 由 srv_start() 調(diào)用:
dberr_t srv_start(bool create_new_db) {
...
lock_sys_create(srv_lock_table_size);
...
}
變量 srv_lock_table_size 在 innodb_init_params() 中賦值,它的值會(huì)傳遞給 lock_sys_create() 的參數(shù) n_cells。
static int innodb_init_params() {
...
srv_lock_table_size =
5 * (srv_buf_pool_size / UNIV_PAGE_SIZE);
...
}
srv_buf_pool_size 是 buffer pool 的大小,UNIV_PAGE_SIZE 是一個(gè)數(shù)據(jù)頁的大小,它們的單位都是字節(jié)。
以 buffer pool 大小為 128M、數(shù)據(jù)頁大小為 16K 為例,變量 srv_lock_table_size 的值計(jì)算如下:
// 128M = 134217728 字節(jié)
// 16K = 16384 字節(jié)
srv_lock_table_size = 5 * (134217728 / 16384)
= 40960
變量 srv_lock_table_size 的值(40960)最終會(huì)傳遞給 lock_sys_create() 的參數(shù) n_cells。用 40960 替換 n_cells 之后如下:
void lock_sys_create(ulint n_cells)
{
...
lock_sys->rec_hash =
ut::new_<hash_table_t>(40960);
...
}
以上代碼說明 buffer pool 大小為 128M,數(shù)據(jù)頁大小為 16K 時(shí),鎖模塊結(jié)構(gòu)的 rec_hash 屬性有 40960 個(gè)格子。
每個(gè)格子都有編號(hào),從 0 開始,一直到 40959。
這些格子并不是用來存儲(chǔ)行鎖結(jié)構(gòu),而是用來管理行鎖結(jié)構(gòu),它們的作用相當(dāng)于線頭,找到了線頭就能牽出一根線。
創(chuàng)建行鎖結(jié)構(gòu)之后,會(huì)先根據(jù)行鎖結(jié)構(gòu)中那些記錄所屬數(shù)據(jù)頁的頁號(hào)和表空間 ID,計(jì)算得到哈希值,再根據(jù)哈希值計(jì)算得到格子的編號(hào)。
多個(gè)行鎖結(jié)構(gòu)可能計(jì)算得到相同的哈希值,從而得到相同的編號(hào),對(duì)應(yīng)到同一個(gè)格子,這些行鎖結(jié)構(gòu)通過各自的 hash 屬性形成一個(gè)行鎖結(jié)構(gòu)鏈表。如果我們把這個(gè)鏈表看成一根線,這個(gè)格子就是這根線的線頭。
計(jì)算出格子編號(hào)之后,行鎖結(jié)構(gòu)會(huì)插入到格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表的最前面。
想要找到某個(gè)行鎖結(jié)構(gòu),也需要根據(jù)同樣的規(guī)則,計(jì)算得到格子編號(hào),再根據(jù)編號(hào)找到格子,最后遍歷這個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表,以找到目標(biāo)行鎖結(jié)構(gòu)。
2.2 誰來保護(hù)表鎖和行鎖結(jié)構(gòu)?
前面我們介紹了 rec_hash 是個(gè)哈希表,分為很多格子,每個(gè)格子管理一個(gè)行鎖結(jié)構(gòu)鏈表。同一個(gè)鏈表的所有行鎖結(jié)構(gòu),計(jì)算得到的哈希值相同。
事務(wù)加行鎖時(shí),會(huì)優(yōu)先考慮共用已有的行鎖結(jié)構(gòu),這就要先找到一個(gè)可以共用的行鎖結(jié)構(gòu)。
首先,需要找到 rec_hash 的某個(gè)格子。
然后,遍歷這個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表,并根據(jù)共用條件,判斷某個(gè)行鎖結(jié)構(gòu)是否可以共用。
事務(wù)加行鎖時(shí),如果生成了新的行鎖結(jié)構(gòu),需要找到 rec_hash 的某個(gè)格子,把行鎖結(jié)構(gòu)插入到這個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表的最前面。
事務(wù)提交或回滾時(shí),釋放所有行鎖,需要找到每個(gè)鎖結(jié)構(gòu)在哪個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表中,并從鏈表中刪除這個(gè)行鎖結(jié)構(gòu)。
事務(wù)加表鎖時(shí),會(huì)遍歷這個(gè)表對(duì)象的 locks 鏈表,以判斷可以立即獲得表鎖,還是需要進(jìn)入等待狀態(tài)。
事務(wù)提交或回滾時(shí),釋放所有表鎖,需要從每個(gè)表對(duì)象的 locks 鏈表中刪除這個(gè)表鎖結(jié)構(gòu)。
多個(gè)事務(wù)執(zhí)行上面這些操作,可能會(huì)同時(shí)讀寫 rec_hash 中某個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表,也可能同時(shí)讀寫某個(gè)表對(duì)象的 locks 鏈表。
為了避免并發(fā)操作同時(shí)讀寫同一個(gè)行鎖結(jié)構(gòu)鏈表、或者同時(shí)讀寫同一個(gè)表對(duì)象的 locks 鏈表出現(xiàn)沖突,需要有個(gè)什么東西,來限制同一時(shí)刻只有一個(gè)事務(wù)讀寫某個(gè)行鎖結(jié)構(gòu)鏈表、或者某個(gè)表對(duì)象的 locks 鏈表。
于是,就有了鎖模塊結(jié)構(gòu)的 latches 屬性,它的類型為 locksys::Latches。
class Latches {
private:
...
Unique_sharded_rw_lock global_latch;
Page_shards page_shards;
Table_shards table_shards;
...
}
latches 也是一個(gè)對(duì)象,有三個(gè)屬性,分別為 global_latch、page_shards、table_shards。
事務(wù)提交或回滾時(shí),釋放所有行鎖和表鎖會(huì)用到 global_latch。
事務(wù)加行鎖時(shí),會(huì)用到 page_shards。
事務(wù)加表鎖時(shí),會(huì)用到 table_shards。
page_shards、table_shards 的類型分為 Page_shards、Table_shards,定義如下:
static constexpr size_t SHARDS_COUNT = 512;
class Page_shards {
...
Padded_mutex mutexes[SHARDS_COUNT];
...
}
class Table_shards {
...
Padded_mutex mutexes[SHARDS_COUNT];
...
}
Page_shards 的 mutexes 屬性是個(gè)數(shù)組,有 512 個(gè)元素。
有新的行鎖結(jié)構(gòu)需要加入某個(gè)行鎖結(jié)構(gòu)鏈表,或者需要遍歷某個(gè)行鎖結(jié)構(gòu)鏈表以找到目標(biāo)行鎖結(jié)構(gòu)時(shí),會(huì)根據(jù)行鎖結(jié)構(gòu)中那些記錄所屬數(shù)據(jù)頁的頁號(hào)和表空間 ID,計(jì)算得到哈希值,再根據(jù)哈希值計(jì)算得到數(shù)組下標(biāo),到 mutexes 數(shù)組中拿到下標(biāo)對(duì)應(yīng)的互斥量,就可以保護(hù)需要讀寫的行鎖結(jié)構(gòu)鏈表了。
Table_shards 的 mutexes 屬性也是個(gè)數(shù)組,同樣有 512 個(gè)元素。
某個(gè)表對(duì)象的 locks 鏈表需要保護(hù)時(shí),會(huì)直接用表 ID 對(duì) 512 取模(table_id % 512),得到的結(jié)果作為數(shù)組下標(biāo),到 mutexes 數(shù)組中拿到下標(biāo)對(duì)應(yīng)的互斥量,就可以保護(hù)這個(gè)表對(duì)象的 locks 鏈表了。
2.3 鎖等待了怎么辦?
鎖模塊結(jié)構(gòu)中,有三個(gè)屬性和鎖等待相關(guān),分別是 wait_mutex、waiting_threads、last_slot,它們的初始化代碼如下:
void lock_sys_create(ulint n_cells)
{
ulint lock_sys_sz;
// 鎖模塊結(jié)構(gòu)占用的內(nèi)存大小
// 加上 waiting_threads 指向的內(nèi)存區(qū)域的大小
// 因?yàn)檫@兩部分要一起分配內(nèi)存
lock_sys_sz = sizeof(*lock_sys)
+ srv_max_n_threads
* sizeof(srv_slot_t);
...
void *ptr = &lock_sys[1];
lock_sys->waiting_threads =
static_cast<srv_slot_t *>(ptr);
// 初始化時(shí)
// last_slot 和 waiting_threads 指向同一個(gè)位置
lock_sys->last_slot =
lock_sys->waiting_threads;
mutex_create(LATCH_ID_LOCK_SYS_WAIT,
&lock_sys->wait_mutex);
...
}
waiting_threads 屬性是個(gè)指針,它指向一片內(nèi)存區(qū)域,這片內(nèi)存區(qū)域分為 srv_max_n_threads 個(gè) slot,每個(gè) slot 存放一個(gè) srv_slot_t 對(duì)象。
srv_max_n_threads 在 innodb_init_params() 中賦值,硬編碼為 102400。
也就是說,waiting_threads 屬性指向的內(nèi)存區(qū)域,最多可以存放 102400 個(gè) srv_slot_t 對(duì)象。
如果某個(gè)事務(wù)不能立即獲得鎖(表鎖或行鎖),就會(huì)在這片內(nèi)存區(qū)域中找到一個(gè)空閑的 slot,構(gòu)造一個(gè)包含該事務(wù)以及鎖信息的 srv_slot_t 對(duì)象放入這個(gè) slot,并標(biāo)記這個(gè) slot 為已使用狀態(tài)。
last_slot 屬性也是個(gè)指針,初始化時(shí),和 waiting_threads 屬性指向相同的內(nèi)存地址。
隨著不斷有事務(wù)進(jìn)入鎖等待狀態(tài)、以及處于鎖等待狀態(tài)的事務(wù)獲得鎖,last_slot 會(huì)不斷變化。
不過,不管怎么變化,last_slot 始終遵循一個(gè)原則,就是它指向的那個(gè) slot,以及之后的所有 slot 都處于空閑狀態(tài)。
為什么需要 last_slot?
因?yàn)楹笈_(tái)線程檢查鎖等待是否超時(shí),會(huì)從后往前遍歷 waiting_threads 屬性指向的內(nèi)存區(qū)域。
如果沒有 last_slot,每次遍歷都需要從最后一個(gè) slot 開始,到第一個(gè) slot 為止,檢查每個(gè) slot 對(duì)應(yīng)的鎖等待是否超時(shí)。
然而,通常情況下,waiting_threads 屬性指向的內(nèi)存區(qū)域中的 102400 個(gè) slot,其中大部分都是空閑的。
空閑 slot 沒有被正在等待鎖的事務(wù)占用,實(shí)際上不需要檢查鎖等待是否超時(shí)。
如果沒有 last_slot,每次檢查鎖等待是否超時(shí),都要遍歷所有 slot,顯然很浪費(fèi)時(shí)間。
為了提升檢查鎖等待超時(shí)的效率,只需要遍歷已使用狀態(tài)的 slot 就可以了,這就需要有個(gè)東西來標(biāo)識(shí)哪個(gè)范圍內(nèi)的 slot 是已使用狀態(tài),于是,就有了 last_slot。
有一點(diǎn)需要說明,如果某個(gè)事務(wù)曾經(jīng)進(jìn)入過鎖等待狀態(tài),占用了某個(gè) slot。某一輪檢查鎖等待超時(shí)之前,這個(gè)事務(wù)獲得了鎖,又會(huì)把它占用的那個(gè) slot 重置為空閑狀態(tài)。
所以,last_slot 之前的那些 slot,并不全部是已使用狀態(tài),也有一些是空閑的,但是這個(gè)數(shù)量應(yīng)該不會(huì)很多,遍歷這些少量的空閑 slot,也不會(huì)浪費(fèi)太多時(shí)間。
介紹完 waiting_threads、last_slot,終于輪到 wait_mutex 屬性了。
從屬性名上看,wait_mutex 屬性顯然是個(gè)互斥量。
多個(gè)事務(wù)同時(shí)讀寫 last_slot 屬性,可能造成沖突,這就需要有個(gè)東西來保證同一時(shí)刻只有一個(gè)線程讀寫 last_slot 屬性,于是就有了 wait_mutex。
2.4 那就發(fā)個(gè)鎖等待通知
事務(wù)想要加鎖(表鎖或行鎖),如果發(fā)生了鎖等待,新出現(xiàn)的鎖等待,和原來那些鎖等待攪和在一起,有可能會(huì)出現(xiàn)死鎖。
為了及時(shí)發(fā)現(xiàn)死鎖,事務(wù)進(jìn)入鎖等待狀態(tài)之前,會(huì)觸一個(gè)事件,通知后臺(tái)線程出現(xiàn)了鎖等待。
這個(gè)事件就保存在鎖模塊結(jié)構(gòu)的 timeout_event 屬性中。
監(jiān)聽 timeout_event 事件的后臺(tái)線程收到通知之后,就會(huì)開始檢查是否發(fā)生了死鎖。如果檢查發(fā)現(xiàn)了死鎖,就及時(shí)解決。
3. 總結(jié)
鎖模塊結(jié)構(gòu)的 rec_hash 屬性是個(gè)哈希表,分為很多小格子,每個(gè)格子管理一個(gè)行鎖結(jié)構(gòu)鏈表。
latches 屬性用于保證同一時(shí)刻只有一個(gè)線程讀寫 rec_hash 屬性的同一個(gè)格子對(duì)應(yīng)的行鎖結(jié)構(gòu)鏈表,以及同一時(shí)刻只有一個(gè)線程讀寫同一個(gè)表對(duì)象的 locks 鏈表。
waiting_threads 屬性指向一片分為 102400 個(gè) slot 的內(nèi)存區(qū)域,每個(gè)等待獲得鎖的事務(wù)會(huì)占用其中一個(gè) slot。
last_slot 屬性用于減少檢查鎖等待超時(shí)需要遍歷的 slot 數(shù)量,提升效率。
wait_mutex 屬性用于保證同一時(shí)刻只有一個(gè)線程讀寫 last_sot 屬性。
timeout_event 屬性用于發(fā)生鎖等待時(shí),通知后臺(tái)線程及時(shí)檢查是否出現(xiàn)了死鎖。
作者:操盛春,愛可生技術(shù)專家,公眾號(hào)『一樹一溪』作者,專注于研究 MySQL 和 OceanBase 源碼。