MySQL 核心模塊揭秘 | 鎖在內(nèi)存里長(zhǎng)什么樣?
1. 共用的結(jié)構(gòu)
InnoDB 的表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu),有一些共同屬性,也有一些不同屬性。
因?yàn)橛泄餐瑢傩裕礞i結(jié)構(gòu)和行鎖結(jié)構(gòu)都使用結(jié)構(gòu)體 lock_t 來表示鎖結(jié)構(gòu)。
在 lock_t 之下,又定義了 lock_table_t、lock_rec_t 分別包含表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)的不同屬性。
為了更直觀的理解表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu),我們?nèi)サ?nbsp;lock_t 的一些非核心信息之后,整理如下:
// storage/innobase/include/lock0priv.h
struct lock_t {
trx_t *trx;
UT_LIST_NODE_T(lock_t) trx_locks;
dict_index_t *index;
lock_t *hash;
union {
lock_table_t tab_lock;
lock_rec_t rec_lock;
};
uint32_t type_mode;
};
雖然表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)都定義了自己的結(jié)構(gòu)體,用于表示各自不同的屬性,但是 lock_t 中 index、hash 這兩個(gè)只用于行鎖結(jié)構(gòu)的屬性,并沒有放入 lock_rec_t。
我們就不去追溯為什么這兩個(gè)屬性會(huì)放在 lock_t 中,而沒有放入 lock_rec_t 了。
2. type_mode
從屬性名上看,表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)的 type_mode 屬性存放的是鎖類型和鎖模式。
實(shí)際上,這個(gè)屬性比較復(fù)雜,它占用 4 字節(jié),共 32 位,分為四個(gè)部分。
圖片
第一部分,1 ~ 4 位,這 4 位是個(gè)整體,共同表示一個(gè)整數(shù)值,就是鎖模式。
- 0(LOCK_IS),表級(jí)別的意向共享鎖。
- 1(LOCK_IX),表級(jí)別的意向排他鎖。
- 2(LOCK_S),表級(jí)別或行級(jí)別的共享鎖。
- 3(LOCK_X),表級(jí)別或行級(jí)別的排他鎖。
- 4(LOCK_AUTO_INC),表級(jí)別的 Auto-Inc 鎖。
鎖模式部分的 4 字節(jié),作為一個(gè)整體使用,而沒有按位使用,這是有原因的。
按整體使用,4 字節(jié)的無符號(hào)整數(shù)最大值為 15,最多可以表示 15 種鎖模式。
按位使用,每位只能表示一種鎖模式,4 位只能表示 4 種鎖模式。
第二部分,5 ~ 8 位,按位使用,存放的是鎖類型。
- 第 5 位標(biāo)識(shí)是否為表鎖(LOCK_TABLE)。
- 第 6 位標(biāo)識(shí)是否為行鎖(LOCK_REC)。
- 第 7 ~ 8 位,暫未使用。
第三部分,第 9 位,按位使用,存放的是鎖等待狀態(tài)(LOCK_WAIT),置為 0 表示已經(jīng)獲得鎖,置為 1 表示處于鎖等待狀態(tài)。
第四部分,10 ~ 32 位,按位使用,存放的是鎖的精確模式,這部分只有行鎖和謂詞鎖會(huì)用到,表鎖不會(huì)用到。
- 第 10 位用于標(biāo)識(shí)間隙鎖(LOCK_GAP)。
- 第 11 位用于標(biāo)識(shí)普通記錄鎖(LOCK_REC_NOT_GAP)。
- 第 12 位用于標(biāo)識(shí)插入意向鎖(LOCK_INSERT_INTENTION)。
- 第 13 位,暫未使用。
- 第 14 ~ 15 位分別用于標(biāo)識(shí) LOCK_PREDICATE、LOCK_PRDT_PAGE,都屬于謂詞鎖。
- 第 16 ~ 32 位,暫未使用。
看到這里,大家有沒有覺得奇怪,怎么沒有用于標(biāo)識(shí) Next-Key 的位置?
鎖模式為行鎖(LOCK_REC)時(shí),如果 10 ~ 32 位中所有位都被設(shè)置為 0,就表示加的行鎖是 Next-Key 鎖。
3. 表鎖結(jié)構(gòu)
lock_t 中,表鎖結(jié)構(gòu)只使用 trx、trx_locks、type_mode 三個(gè)屬性,加上 lock_table_t 的 table、locks 屬性,就是表鎖結(jié)構(gòu)的全部屬性了。
圖片
MySQL 執(zhí)行 DDL、DML 語句時(shí),InnoDB 都會(huì)有對(duì)應(yīng)的事務(wù)(用戶手動(dòng)啟動(dòng)或者 InnoDB 自動(dòng)啟動(dòng))來執(zhí)行這些語句對(duì)應(yīng)的操作。
加鎖操作自然也是在事務(wù)中進(jìn)行的,trx 屬性就是加這個(gè)表鎖的事務(wù)對(duì)象。
事務(wù)執(zhí)行一條或多條 DML 語句,可能涉及多個(gè)表,也就有可能加多個(gè)表鎖。事務(wù)除了加表鎖,還有可能加行鎖,同一個(gè)事務(wù)加的一個(gè)或多個(gè)表鎖和一個(gè)或多個(gè)行鎖的鎖結(jié)構(gòu)通過 trx_locks 屬性形成一個(gè)表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)混合的鏈表。
表鎖是加在表上的,自然就需要知道表鎖結(jié)構(gòu)屬于哪個(gè)表了,table 屬性就是這個(gè)表鎖結(jié)構(gòu)所屬的表對(duì)象。
同一時(shí)刻,可能有多個(gè)事務(wù)已經(jīng)或者想要對(duì)同一個(gè)表加鎖。對(duì)于兼容的表鎖,多個(gè)事務(wù)可以同時(shí)加鎖,對(duì)于不兼容的表鎖,后加鎖的事務(wù)就會(huì)處于等待狀態(tài)。
事務(wù)想要對(duì)某個(gè)表加鎖,InnoDB 怎么判斷事務(wù)可以立即獲得鎖,還是要進(jìn)入等待狀態(tài)?
這就是 locks 屬性的用武之地了。
多個(gè)事務(wù)對(duì)同一個(gè)表加了表鎖,這些表鎖的鎖結(jié)構(gòu)會(huì)通過 locks 屬性形成一個(gè)鏈表。
事務(wù)想要對(duì)某個(gè)表加表鎖,InnoDB 就會(huì)遍歷這個(gè)鏈表。
如果鏈表中還沒有表鎖結(jié)構(gòu),或者所有鎖結(jié)構(gòu)對(duì)應(yīng)的表鎖都和事務(wù)當(dāng)前要加的表鎖兼容,事務(wù)就可以立即獲得鎖,否則就需要進(jìn)入等待狀態(tài)。
type_mode 屬性的第 5 位用于標(biāo)識(shí)鎖結(jié)構(gòu)是否為表鎖(LOCK_TABLE)。
對(duì)于表鎖,鎖結(jié)構(gòu)中 type_mode 屬性的第 5 位會(huì)被設(shè)置為 1,第 1 ~ 4 位會(huì)寫入鎖模式對(duì)應(yīng)的整數(shù)值。
如果事務(wù)不能立即獲得表鎖,type_mode 屬性的第 9 位會(huì)被設(shè)置為 1,表示處于鎖等待狀態(tài)。
4. 行鎖結(jié)構(gòu)
lock_t 中,行鎖結(jié)構(gòu)使用 trx、trx_locks、index、hash、type_mode 五個(gè)屬性,加上 lock_rec_t 的 page_id、n_bits 兩個(gè)屬性,外加行鎖結(jié)構(gòu)最后外掛了一塊沒有屬性名的內(nèi)存區(qū)域(我們暫且命名為 bitmap),就是行鎖的整體結(jié)構(gòu)了。
圖片
4.1 有名有姓的那些屬性
和表鎖結(jié)構(gòu)一樣,行鎖結(jié)構(gòu)里也有 trx 和 trx_locks 兩個(gè)屬性。
trx 屬性是加這個(gè)行鎖的事務(wù)對(duì)象。同一個(gè)事務(wù)加的一個(gè)或多個(gè)表鎖和一個(gè)或多個(gè)行鎖的鎖結(jié)構(gòu),通過 trx_locks 屬性形成一個(gè)表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)混合的鏈表。
主表的記錄存儲(chǔ)在主鍵索引中,二級(jí)索引(包括唯一索引、非唯一索引)的記錄存儲(chǔ)在二級(jí)索引中,行鎖都是對(duì)主鍵索引或二級(jí)索引的記錄加鎖。index 屬性就是這個(gè)行鎖結(jié)構(gòu)所屬的索引對(duì)象。
InnoDB 可能同時(shí)有很多個(gè)事務(wù)在運(yùn)行,這些事務(wù)加的行鎖,可能會(huì)產(chǎn)生多個(gè)行鎖結(jié)構(gòu)。
每個(gè)行鎖結(jié)構(gòu)都會(huì)根據(jù) page_id 屬性中保存的表空間 ID、數(shù)據(jù)頁(yè)號(hào)計(jì)算得到一個(gè)哈希值。哈希值相同的多個(gè)行鎖結(jié)構(gòu)通過 hash 屬性形成一個(gè)行鎖結(jié)構(gòu)鏈表。
n_bits 屬性的值是個(gè)無符號(hào)整數(shù),表示這個(gè)鎖結(jié)構(gòu)能保存多少條記錄的行鎖狀態(tài),也就是最多有多少記錄能共用這個(gè)行鎖結(jié)構(gòu)。
對(duì)于行鎖,鎖結(jié)構(gòu)中 type_mode 屬性的第 6 位會(huì)被設(shè)置為 1,第 1 ~ 4 位會(huì)被寫入鎖模式對(duì)應(yīng)的整數(shù)值。
行鎖的不同精確模式,type_mode 屬性第四部分(10 ~ 32 位)各位的賦值情況如下:
- 普通記錄鎖,type_mode 屬性的第 10 位會(huì)被設(shè)置為 1。
- 間隙鎖,type_mode 屬性的第 11 位會(huì)被設(shè)置為 1。
- 插入意向鎖,type_mode 屬性的第 12 位會(huì)被設(shè)置為 1。
- Next-Key 鎖,type_mode 屬性的第 10 ~ 32 位都設(shè)置為 0。
如果事務(wù)不能立即獲得行鎖,type_mode 屬性的第 9 位會(huì)被設(shè)置為 1,表示處于鎖等待狀態(tài)。
4.2 隱姓埋名的內(nèi)存區(qū)域
前面介紹的那些,都是 InnoDB 給取了名字的行鎖結(jié)構(gòu)屬性。
還有一塊沒有名字的內(nèi)存區(qū)域沒有介紹。在前面的行鎖結(jié)構(gòu)圖中,我們給這塊內(nèi)存區(qū)域取了個(gè)名字,為 bitmap。
bitmap 這塊內(nèi)存區(qū)域是干嘛用的呢?
待我們細(xì)細(xì)說來。
我們先忽略 bitmap 內(nèi)存區(qū)域的存在,假設(shè)一個(gè)事務(wù)對(duì)一條記錄加行鎖,會(huì)產(chǎn)生一個(gè)行鎖結(jié)構(gòu),對(duì)多條記錄加行鎖,就會(huì)產(chǎn)生多個(gè)行鎖結(jié)構(gòu)。
又假設(shè)事務(wù)對(duì)多條記錄加的都是共享 Next-Key 鎖,并且已經(jīng)獲得了鎖,巧合的是這些記錄又位于同一個(gè)數(shù)據(jù)頁(yè),那么,這些鎖結(jié)構(gòu)除了加鎖記錄不一樣,其它屬性的值都相同。
如果真這么設(shè)計(jì)行鎖結(jié)構(gòu),是不是太浪費(fèi)內(nèi)存空間了?
當(dāng)然是了。雖然現(xiàn)在內(nèi)存越來越便宜,但是畢竟還要花錢,也不能那么鋪張浪費(fèi)。
本著勤儉節(jié)約的原則,InnoDB 把加鎖記錄不同、其它屬性值都相同的多個(gè)行鎖結(jié)構(gòu)合并成一個(gè),另外開辟一塊內(nèi)存區(qū)域用于標(biāo)識(shí)加鎖記錄,于是就有了我們命名為 bitmap 的內(nèi)存區(qū)域。
bitmap 內(nèi)存區(qū)域按位使用,每一位都用于標(biāo)識(shí)事務(wù)是否對(duì)某條記錄加了行鎖。如果某一位被設(shè)置為 1,就表示事務(wù)對(duì)該位對(duì)應(yīng)的記錄加了行鎖。
圖片
上圖是事務(wù)對(duì)象初始化時(shí),預(yù)先創(chuàng)建的一個(gè)行鎖結(jié)構(gòu)的 bitmap 內(nèi)存區(qū)域示意圖,大小為 256 字節(jié),可以用于標(biāo)識(shí)這個(gè)事務(wù)對(duì) 2048 條記錄加行鎖的情況。
示意圖中,第 3 位和第 5 位被設(shè)置為 1,說明事務(wù)對(duì)數(shù)據(jù)頁(yè)中序號(hào)為 0 和 4 的記錄加了行鎖。
沒有規(guī)矩不成方圓,InnoDB 不會(huì)胡亂的把多個(gè)行鎖結(jié)構(gòu)合并成一個(gè)。
事務(wù)對(duì)多條記錄加行鎖,想要共用一個(gè)行鎖結(jié)構(gòu),需要同時(shí)滿足以下個(gè)條件:
- 同一個(gè)事務(wù)對(duì)多條記錄加行鎖。
- 這些記錄位于同一個(gè)數(shù)據(jù)頁(yè)中(也就是同一個(gè)表同一個(gè)索引的同一個(gè)數(shù)據(jù)頁(yè))。
- 這些行鎖的鎖模式相同,必須都是共享鎖,或者都是排他鎖。
- 這些行鎖的精確模式相同,必須都是普通記錄鎖,或者都是間隙鎖,或者都是 Next-Key 鎖。
- 這些行鎖都處于獲得鎖的狀態(tài),不能處于鎖等待狀態(tài)。
4.3 共用行鎖結(jié)構(gòu)的兩個(gè)問題
問題一:多個(gè)處于等待狀態(tài)的行鎖能共用一個(gè)鎖結(jié)構(gòu)嗎?
理論上是可以的,但實(shí)際上不會(huì)出現(xiàn)這種情況。
因?yàn)楣灿靡粋€(gè)行鎖結(jié)構(gòu)需要滿足的條件之一,是一個(gè)事務(wù)對(duì)多條記錄加行鎖。
然而,一個(gè)事務(wù)對(duì)某條記錄加行鎖處于等待狀態(tài),在獲得鎖或者鎖超時(shí)之前(不考慮異常情況),這個(gè)事務(wù)不會(huì)繼續(xù)往下執(zhí)行。
這樣一來,一個(gè)事務(wù)在某一時(shí)刻,最多只有一個(gè)行鎖結(jié)構(gòu)(對(duì)應(yīng)一條記錄)處于等待狀態(tài),也就不存在多個(gè)處于等待狀態(tài)的行鎖共用一個(gè)行鎖結(jié)構(gòu)的情況了。
獲得鎖或者鎖等待超時(shí)之后,行鎖結(jié)構(gòu)中 type_mode 的第 9 位就會(huì)被設(shè)置為 0,表示這個(gè)行鎖處于非等待狀態(tài),后續(xù)在滿足共用條件的情況下,這個(gè)鎖結(jié)構(gòu)才可以被共用。
問題二:多個(gè)插入意向鎖能共用一個(gè)鎖結(jié)構(gòu)嗎?
同樣,理論上是可以的,但實(shí)際上不會(huì)出現(xiàn)這種情況。
首先,插入意向鎖的加鎖場(chǎng)景,是事務(wù) T 想要在某條記錄前面的間隙插入一條記錄,而這個(gè)間隙被其它事務(wù)加了間隙鎖或者 Next-Key 鎖,導(dǎo)致事務(wù) T 必須在這個(gè)間隙上加插入意向鎖,并等待其它事務(wù)釋放間隙鎖或者 Next-Key 鎖。
前面已經(jīng)介紹過,處于等待狀態(tài)的行鎖結(jié)構(gòu),是不能共用的。
然后,事務(wù) T 獲得鎖之后,它的精確模式為 LOCK_GAP + LOCK_INSERT_INTENTION,其它間隙鎖也不能共用這個(gè)鎖結(jié)構(gòu),因?yàn)殚g隙鎖的精確模式為 LOCK_GAP。
雖然插入意向鎖的鎖結(jié)構(gòu)不能共用,會(huì)浪費(fèi)一些內(nèi)存,但好在加插入意向鎖的情況也不會(huì)非常多,浪費(fèi)的內(nèi)存也就不會(huì)太多。
5. 總結(jié)
InnoDB 的表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu),有一部分屬性是相同的,也有一部分屬性是專用的,所以,代碼里定義了三個(gè)結(jié)構(gòu)體來描述表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)。
一個(gè)事務(wù)對(duì)每個(gè)表加表鎖,都會(huì)產(chǎn)生一個(gè)表鎖結(jié)構(gòu)。
一個(gè)事務(wù)對(duì)多條記錄加行鎖,滿足條件時(shí),多條記錄的行鎖可以共用一個(gè)行鎖結(jié)構(gòu),以節(jié)省內(nèi)存。
處于等待狀態(tài)的行鎖結(jié)構(gòu),不能共用。獲得行鎖或者鎖等待超時(shí)之后,這個(gè)鎖結(jié)構(gòu)會(huì)變?yōu)榉堑却隣顟B(tài),之后滿足條件時(shí),這個(gè)鎖結(jié)構(gòu)可以被共用。
插入意向鎖的鎖結(jié)構(gòu)不能共用。