1. MySql架構(gòu)

MySQL 的架構(gòu)共分為兩層:Server 層和存儲(chǔ)引擎層,
Server 層負(fù)責(zé)建立連接、分析和執(zhí)行 SQL。MySQL 大多數(shù)的核心功能模塊都在這實(shí)現(xiàn),主要包括連接器,查詢緩存、解析器、預(yù)處理器、優(yōu)化器、執(zhí)行器等。另外,所有的內(nèi)置函數(shù)(如日期、時(shí)間、數(shù)學(xué)和加密函數(shù)等)和所有跨存儲(chǔ)引擎的功能(如存儲(chǔ)過(guò)程、觸發(fā)器、視圖等。)都在 Server 層實(shí)現(xiàn)。
存儲(chǔ)引擎層負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和提取。支持 InnoDB、MyISAM、Memory 等多個(gè)存儲(chǔ)引擎,不同的存儲(chǔ)引擎共用一個(gè) Server 層。現(xiàn)在最常用的存儲(chǔ)引擎是 InnoDB,從 MySQL 5.5 版本開(kāi)始, InnoDB 成為了 MySQL 的默認(rèn)存儲(chǔ)引擎。我們常說(shuō)的索引數(shù)據(jù)結(jié)構(gòu),就是由存儲(chǔ)引擎層實(shí)現(xiàn)的,不同的存儲(chǔ)引擎支持的索引類型也不相同,比如 InnoDB 支持索引類型是 B+樹(shù) ,且是默認(rèn)使用,也就是說(shuō)在數(shù)據(jù)表中創(chuàng)建的主鍵索引和二級(jí)索引默認(rèn)使用的是 B+ 樹(shù)索引。
執(zhí)行一條 SQL 查詢語(yǔ)句,期間發(fā)生了什么?
1. 連接器:建立連接,管理連接、校驗(yàn)用戶身份;
2. 查詢緩存:查詢語(yǔ)句如果命中查詢緩存則直接返回,否則繼續(xù)往下執(zhí)行。MySQL 8.0 已刪除該模塊;
3. 解析 SQL,通過(guò)解析器對(duì) SQL 查詢語(yǔ)句進(jìn)行詞法分析、語(yǔ)法分析,然后構(gòu)建語(yǔ)法樹(shù),方便后續(xù)模塊讀取表名、字段、語(yǔ)句類型;
4. 執(zhí)行 SQL:執(zhí)行 SQL 共有三個(gè)階段:
? ?預(yù)處理階段:檢查表或字段是否存在;將?select *?中的?*?符號(hào)擴(kuò)展為表上的所有列。
? ?優(yōu)化階段:基于查詢成本的考慮, 選擇查詢成本最小的執(zhí)行計(jì)劃;
? ?執(zhí)行階段:根據(jù)執(zhí)行計(jì)劃執(zhí)行 SQL 查詢語(yǔ)句,從存儲(chǔ)引擎讀取記錄,返回給客戶端;
2.?COMPACT 行格式長(zhǎng)什么樣?

變長(zhǎng)字段長(zhǎng)度列表:記錄變成列的字節(jié)數(shù),逆序存放
NULL值列表:一般是1字節(jié)(8位),每一位代表某一列是否為空,也是逆序位
記錄頭信息:
1. delete_mask :標(biāo)識(shí)此條數(shù)據(jù)是否被刪除。從這里可以知道,我們執(zhí)行 detele 刪除記錄的時(shí)候,并不會(huì)真正的刪除記錄,只是將這個(gè)記錄的 delete_mask 標(biāo)記為 1。
2. next_record:下一條記錄的位置。從這里可以知道,記錄與記錄之間是通過(guò)鏈表組織的。在前面我也提到了,指向的是下一條記錄的「記錄頭信息」和「真實(shí)數(shù)據(jù)」之間的位置,這樣的好處是向左讀就是記錄頭信息,向右讀就是真實(shí)數(shù)據(jù),比較方便。
3. record_type:表示當(dāng)前記錄的類型,0表示普通記錄,1表示B+樹(shù)非葉子節(jié)點(diǎn)記錄,2表示最小記錄,3表示最大記錄
row_id
如果我們建表的時(shí)候指定了主鍵或者唯一約束列,那么就沒(méi)有 row_id 隱藏字段了。如果既沒(méi)有指定主鍵,又沒(méi)有唯一約束,那么 InnoDB 就會(huì)為記錄添加 row_id 隱藏字段。row_id不是必需的,占用 6 個(gè)字節(jié)。
trx_id
事務(wù)id,表示這個(gè)數(shù)據(jù)是由哪個(gè)事務(wù)生成的。 trx_id是必需的,占用 6 個(gè)字節(jié)。
roll_pointer
這條記錄上一個(gè)版本的指針。roll_pointer 是必需的,占用 7 個(gè)字節(jié)。
為什么「變長(zhǎng)字段長(zhǎng)度列表」的信息要按照逆序存放?
這個(gè)設(shè)計(jì)是有想法的,主要是因?yàn)椤赣涗涱^信息」中指向下一個(gè)記錄的指針,指向的是下一條記錄的「記錄頭信息」和「真實(shí)數(shù)據(jù)」之間的位置,這樣的好處是向左讀就是記錄頭信息,向右讀就是真實(shí)數(shù)據(jù),比較方便。
「變長(zhǎng)字段長(zhǎng)度列表」中的信息之所以要逆序存放,是因?yàn)檫@樣可以使得位置靠前的記錄的真實(shí)數(shù)據(jù)和數(shù)據(jù)對(duì)應(yīng)的字段長(zhǎng)度信息可以同時(shí)在一個(gè) CPU Cache Line 中,這樣就可以提高 CPU Cache 的命中率。
同樣的道理, NULL 值列表的信息也需要逆序存放。
3.?varchar(n) 中 n 最大取值為多少?
MySQL 規(guī)定除了 TEXT、BLOBs 這種大對(duì)象類型之外,其他所有的列(不包括隱藏列和記錄頭信息)占用的字節(jié)長(zhǎng)度加起來(lái)不能超過(guò) 65535 個(gè)字節(jié)。
也就是說(shuō),一行記錄除了 TEXT、BLOBs 類型的列,限制最大為 65535 字節(jié),注意是一行的總長(zhǎng)度,不是一列。
一行記錄最大能存儲(chǔ) 65535 字節(jié)的數(shù)據(jù),但是這個(gè)是包含「變長(zhǎng)字段字節(jié)數(shù)列表所占用的字節(jié)數(shù)」和「NULL值列表所占用的字節(jié)數(shù)」。所以, 我們?cè)谒?varchar(n) 中 n 最大值時(shí),需要減去這兩個(gè)列表所占用的字節(jié)數(shù)。
如果一張表只有一個(gè) varchar(n) 字段,且允許為 NULL,字符集為 ascii。varchar(n) 中 n 最大取值為 65532。
計(jì)算公式:65535 - 變長(zhǎng)字段字節(jié)數(shù)列表所占用的字節(jié)數(shù) - NULL值列表所占用的字節(jié)數(shù) = 65535 - 2 - 1 = 65532。
如果有多個(gè)字段的話,要保證所有字段的長(zhǎng)度 + 變長(zhǎng)字段字節(jié)數(shù)列表所占用的字節(jié)數(shù) + NULL值列表所占用的字節(jié)數(shù) <= 65535。
4. MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中會(huì)用「NULL值列表」來(lái)標(biāo)記值為 NULL 的列,NULL 值并不會(huì)存儲(chǔ)在行格式中的真實(shí)數(shù)據(jù)部分。
NULL值列表會(huì)占用 1 字節(jié)空間,當(dāng)表中所有字段都定義成 NOT NULL,行格式中就不會(huì)有 NULL值列表,這樣可節(jié)省 1 字節(jié)的空間。
5.?行溢出后,MySQL 是怎么處理的?
如果一個(gè)數(shù)據(jù)頁(yè)存不了一條記錄,InnoDB 存儲(chǔ)引擎會(huì)自動(dòng)將溢出的數(shù)據(jù)存放到「溢出頁(yè)」中。
Compact 行格式針對(duì)行溢出的處理是這樣的:當(dāng)發(fā)生行溢出時(shí),在記錄的真實(shí)數(shù)據(jù)處只會(huì)保存該列的一部分?jǐn)?shù)據(jù),而把剩余的數(shù)據(jù)放在「溢出頁(yè)」中,然后真實(shí)數(shù)據(jù)處用 20 字節(jié)存儲(chǔ)指向溢出頁(yè)的地址,從而可以找到剩余數(shù)據(jù)所在的頁(yè)。
Compressed 和 Dynamic 這兩種格式采用完全的行溢出方式,記錄的真實(shí)數(shù)據(jù)處不會(huì)存儲(chǔ)該列的一部分?jǐn)?shù)據(jù),只存儲(chǔ) 20 個(gè)字節(jié)的指針來(lái)指向溢出頁(yè)。而實(shí)際的數(shù)據(jù)都存儲(chǔ)在溢出頁(yè)中。
6. 索引的分類
我們可以按照四個(gè)角度來(lái)分類索引。
1. 按「數(shù)據(jù)結(jié)構(gòu)」分類:B+tree索引、Hash索引、Full-text索引。
2. 按「物理存儲(chǔ)」分類:聚簇索引(主鍵索引)、二級(jí)索引(輔助索引)。
3. 按「字段特性」分類:主鍵索引、唯一索引、普通索引、前綴索引。
4. 按「字段個(gè)數(shù)」分類:單列索引、聯(lián)合索引。
在創(chuàng)建表時(shí),InnoDB 存儲(chǔ)引擎會(huì)根據(jù)不同的場(chǎng)景選擇不同的列作為索引:
1. 如果有主鍵,默認(rèn)會(huì)使用主鍵作為聚簇索引的索引鍵(key);
2. 如果沒(méi)有主鍵,就選擇第一個(gè)不包含 NULL 值的唯一列作為聚簇索引的索引鍵(key);
3. 在上面兩個(gè)都沒(méi)有的情況下,InnoDB 將自動(dòng)生成一個(gè)隱式自增 id 列作為聚簇索引的索引鍵(key);
其它索引都屬于輔助索引(Secondary Index),也被稱為二級(jí)索引或非聚簇索引。創(chuàng)建的主鍵索引和二級(jí)索引默認(rèn)使用的是 B+Tree 索引。
B+Tree 索引
B+Tree 是一種多叉樹(shù),葉子節(jié)點(diǎn)才存放數(shù)據(jù),非葉子節(jié)點(diǎn)只存放索引,而且每個(gè)節(jié)點(diǎn)里的數(shù)據(jù)是按主鍵順序存放的。每一層父節(jié)點(diǎn)的索引值都會(huì)出現(xiàn)在下層子節(jié)點(diǎn)的索引值中,因此在葉子節(jié)點(diǎn)中,包括了所有的索引值信息,并且每一個(gè)葉子節(jié)點(diǎn)都有兩個(gè)指針,分別指向下一個(gè)葉子節(jié)點(diǎn)和上一個(gè)葉子節(jié)點(diǎn),形成一個(gè)雙向鏈表。
主鍵索引的 B+Tree 如圖所示(圖中葉子節(jié)點(diǎn)之間我畫(huà)了單向鏈表,但是實(shí)際上是雙向鏈表,原圖我找不到了,修改不了,偷個(gè)懶我不重畫(huà)了,大家腦補(bǔ)成雙向鏈表就行):

B+Tree 存儲(chǔ)千萬(wàn)級(jí)的數(shù)據(jù)只需要 3-4 層高度就可以滿足,這意味著從千萬(wàn)級(jí)的表查詢目標(biāo)數(shù)據(jù)最多需要 3-4 次磁盤 I/O,所以B+Tree 相比于 B 樹(shù)和二叉樹(shù)來(lái)說(shuō),最大的優(yōu)勢(shì)在于查詢效率很高,因?yàn)榧词乖跀?shù)據(jù)量很大的情況,查詢一個(gè)數(shù)據(jù)的磁盤 I/O 依然維持在 3-4次。
通過(guò)二級(jí)索引查詢商品數(shù)據(jù)的過(guò)程
主鍵索引的 B+Tree 和二級(jí)索引的 B+Tree 區(qū)別如下:
1. 主鍵索引的 B+Tree 的葉子節(jié)點(diǎn)存放的是實(shí)際數(shù)據(jù),所有完整的用戶記錄都存放在主鍵索引的 B+Tree 的葉子節(jié)點(diǎn)里;
2. 二級(jí)索引的 B+Tree 的葉子節(jié)點(diǎn)存放的是主鍵值,而不是實(shí)際數(shù)據(jù)。

會(huì)先檢二級(jí)索引中的 B+Tree 的索引值(商品編碼,product_no),找到對(duì)應(yīng)的葉子節(jié)點(diǎn),然后獲取主鍵值,然后再通過(guò)主鍵索引中的 B+Tree 樹(shù)查詢到對(duì)應(yīng)的葉子節(jié)點(diǎn),然后獲取整行數(shù)據(jù)。這個(gè)過(guò)程叫「回表」,也就是說(shuō)要查兩個(gè) B+Tree 才能查到數(shù)據(jù)。
?B+tree 和 B Tree、二叉樹(shù)、Hash的區(qū)別
1、B+Tree vs B Tree
B+Tree 只在葉子節(jié)點(diǎn)存儲(chǔ)數(shù)據(jù),而 B 樹(shù) 的非葉子節(jié)點(diǎn)也要存儲(chǔ)數(shù)據(jù),所以 B+Tree 的單個(gè)節(jié)點(diǎn)的數(shù)據(jù)量更小,在相同的磁盤 I/O 次數(shù)下,就能查詢更多的節(jié)點(diǎn)。
另外,B+Tree 葉子節(jié)點(diǎn)采用的是雙鏈表連接,適合 MySQL 中常見(jiàn)的基于范圍的順序查找,而 B 樹(shù)無(wú)法做到這一點(diǎn)。
2、B+Tree vs 二叉樹(shù)
對(duì)于有 N 個(gè)葉子節(jié)點(diǎn)的 B+Tree,其搜索復(fù)雜度為O(logdN),其中 d 表示節(jié)點(diǎn)允許的最大子節(jié)點(diǎn)個(gè)數(shù)為 d 個(gè)。
在實(shí)際的應(yīng)用當(dāng)中, d 值是大于100的,這樣就保證了,即使數(shù)據(jù)達(dá)到千萬(wàn)級(jí)別時(shí),B+Tree 的高度依然維持在 3~4 層左右,也就是說(shuō)一次數(shù)據(jù)查詢操作只需要做 3~4 次的磁盤 I/O 操作就能查詢到目標(biāo)數(shù)據(jù)。
而二叉樹(shù)的每個(gè)父節(jié)點(diǎn)的兒子節(jié)點(diǎn)個(gè)數(shù)只能是 2 個(gè),意味著其搜索復(fù)雜度為?O(logN),這已經(jīng)比 B+Tree 高出不少,因此二叉樹(shù)檢索到目標(biāo)數(shù)據(jù)所經(jīng)歷的磁盤 I/O 次數(shù)要更多。
3、B+Tree vs Hash
Hash 在做等值查詢的時(shí)候效率賊快,搜索復(fù)雜度為 O(1)。
但是 Hash 表不適合做范圍查詢,它更適合做等值的查詢,B+Tree 葉子節(jié)點(diǎn)之間是雙向鏈表,更適合范圍查詢,這也是 B+Tree 索引要比 Hash 表索引有著更廣泛的適用場(chǎng)景的原因。
聯(lián)合索引
聯(lián)合索引的最左匹配原則,在遇到范圍查詢(如 >、<)的時(shí)候,就會(huì)停止匹配,也就是范圍查詢的字段可以用到聯(lián)合索引,但是在范圍查詢字段的后面的字段無(wú)法用到聯(lián)合索引。注意,對(duì)于 >=、<=、BETWEEN、like 前綴匹配的范圍查詢,并不會(huì)停止匹配
建立聯(lián)合索引時(shí),要把區(qū)分度大的字段排在前面,這樣區(qū)分度大的字段越有可能被更多的 SQL 使用到。
區(qū)分度就是某個(gè)字段 column 不同值的個(gè)數(shù)「除以」表的總行數(shù),計(jì)算公式如下:

MySQL 還有一個(gè)查詢優(yōu)化器,查詢優(yōu)化器發(fā)現(xiàn)某個(gè)值出現(xiàn)在表的數(shù)據(jù)行中的百分比(慣用的百分比界線是"30%")很高的時(shí)候,它一般會(huì)忽略索引,進(jìn)行全表掃描。
索引下推
?對(duì)于聯(lián)合索引(a, b),在執(zhí)行?select * from table where a > 1 and b = 2?語(yǔ)句的時(shí)候,只有 a 字段能用到索引,那在聯(lián)合索引的 B+Tree 找到第一個(gè)滿足條件的主鍵值(ID 為 2)后,還需要判斷其他條件是否滿足(看 b 是否等于 2),那是在聯(lián)合索引里判斷?還是回主鍵索引去判斷呢?
1. 在 MySQL 5.6 之前,只能從 ID2 (主鍵值)開(kāi)始一個(gè)個(gè)回表,到「主鍵索引」上找出數(shù)據(jù)行,再對(duì)比 b 字段值。
2. 而 MySQL 5.6 引入的索引下推優(yōu)化(index condition pushdown),?可以在聯(lián)合索引遍歷過(guò)程中,對(duì)聯(lián)合索引中包含的字段先做判斷,直接過(guò)濾掉不滿足條件的記錄,減少回表次數(shù)。
7. 什么時(shí)候需要 / 不需要?jiǎng)?chuàng)建索引?
索引最大的好處是提高查詢速度,但是索引也是有缺點(diǎn)的,比如:
1. 需要占用物理空間,數(shù)量越大,占用空間越大;
2. 創(chuàng)建索引和維護(hù)索引要耗費(fèi)時(shí)間,這種時(shí)間隨著數(shù)據(jù)量的增加而增大;
3. 會(huì)降低表的增刪改的效率,因?yàn)槊看卧鰟h改索引,B+ 樹(shù)為了維護(hù)索引有序性,都需要進(jìn)行動(dòng)態(tài)維護(hù)。
什么時(shí)候適用索引?
1. 字段有唯一性限制的,比如商品編碼;
2. 經(jīng)常用于?WHERE?查詢條件的字段,這樣能夠提高整個(gè)表的查詢速度,如果查詢條件不是一個(gè)字段,可以建立聯(lián)合索引。
3. 經(jīng)常用于?GROUP BY?和?ORDER BY?的字段,這樣在查詢的時(shí)候就不需要再去做一次排序了,因?yàn)槲覀兌家呀?jīng)知道了建立索引之后在 B+Tree 中的記錄都是排序好的。
什么時(shí)候不需要?jiǎng)?chuàng)建索引?
1. WHERE?條件,GROUP BY,ORDER BY?里用不到的字段,索引的價(jià)值是快速定位,如果起不到定位的字段通常是不需要?jiǎng)?chuàng)建索引的,因?yàn)樗饕菚?huì)占用物理空間的。
2. 字段中存在大量重復(fù)數(shù)據(jù),不需要?jiǎng)?chuàng)建索引,比如性別字段,只有男女,如果數(shù)據(jù)庫(kù)表中,男女的記錄分布均勻,那么無(wú)論搜索哪個(gè)值都可能得到一半的數(shù)據(jù)。在這些情況下,還不如不要索引,因?yàn)?MySQL 還有一個(gè)查詢優(yōu)化器,查詢優(yōu)化器發(fā)現(xiàn)某個(gè)值出現(xiàn)在表的數(shù)據(jù)行中的百分比很高的時(shí)候,它一般會(huì)忽略索引,進(jìn)行全表掃描。
3. 表數(shù)據(jù)太少的時(shí)候,不需要?jiǎng)?chuàng)建索引;
4. 經(jīng)常更新的字段不用創(chuàng)建索引,比如不要對(duì)電商項(xiàng)目的用戶余額建立索引,因?yàn)樗饕侄晤l繁修改,由于要維護(hù) B+Tree的有序性,那么就需要頻繁的重建索引,這個(gè)過(guò)程是會(huì)影響數(shù)據(jù)庫(kù)性能的。
8. 有什么優(yōu)化索引的方法?
1. 前綴索引優(yōu)化;
前綴索引顧名思義就是使用某個(gè)字段中字符串的前幾個(gè)字符建立索引,使用前綴索引是為了減小索引字段大小,可以增加一個(gè)索引頁(yè)中存儲(chǔ)的索引值,有效提高索引的查詢速度。在一些大字符串的字段作為索引時(shí),使用前綴索引可以幫助我們減小索引項(xiàng)的大小。
2. 覆蓋索引優(yōu)化;
覆蓋索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的葉子節(jié)點(diǎn)上都能找得到的那些索引,從二級(jí)索引中查詢得到記錄,而不需要通過(guò)聚簇索引查詢獲得,可以避免回表的操作。
假設(shè)我們只需要查詢商品的名稱、價(jià)格,有什么方式可以避免回表呢?
我們可以建立一個(gè)聯(lián)合索引,即「商品ID、名稱、價(jià)格」作為一個(gè)聯(lián)合索引。如果索引中存在這些數(shù)據(jù),查詢將不會(huì)再次檢索主鍵索引,從而避免回表。
3. 主鍵索引最好是自增的;
InnoDB 創(chuàng)建主鍵索引默認(rèn)為聚簇索引,數(shù)據(jù)被存放在了 B+Tree 的葉子節(jié)點(diǎn)上。也就是說(shuō),同一個(gè)葉子節(jié)點(diǎn)內(nèi)的各個(gè)數(shù)據(jù)是按主鍵順序存放的,因此,每當(dāng)有一條新的數(shù)據(jù)插入時(shí),數(shù)據(jù)庫(kù)會(huì)根據(jù)主鍵將其插入到對(duì)應(yīng)的葉子節(jié)點(diǎn)中。
如果我們使用自增主鍵,那么每次插入的新數(shù)據(jù)就會(huì)按順序添加到當(dāng)前索引節(jié)點(diǎn)的位置,不需要移動(dòng)已有的數(shù)據(jù),當(dāng)頁(yè)面寫(xiě)滿,就會(huì)自動(dòng)開(kāi)辟一個(gè)新頁(yè)面。因?yàn)槊看尾迦胍粭l新記錄,都是追加操作,不需要重新移動(dòng)數(shù)據(jù),因此這種插入數(shù)據(jù)的方法效率非常高。
如果我們使用非自增主鍵,由于每次插入主鍵的索引值都是隨機(jī)的,因此每次插入新的數(shù)據(jù)時(shí),就可能會(huì)插入到現(xiàn)有數(shù)據(jù)頁(yè)中間的某個(gè)位置,這將不得不移動(dòng)其它數(shù)據(jù)來(lái)滿足新數(shù)據(jù)的插入,甚至需要從一個(gè)頁(yè)面復(fù)制數(shù)據(jù)到另外一個(gè)頁(yè)面,我們通常將這種情況稱為頁(yè)分裂。頁(yè)分裂還有可能會(huì)造成大量的內(nèi)存碎片,導(dǎo)致索引結(jié)構(gòu)不緊湊,從而影響查詢效率。
另外,主鍵字段的長(zhǎng)度不要太大,因?yàn)?b>主鍵字段長(zhǎng)度越小,意味著二級(jí)索引的葉子節(jié)點(diǎn)越?。ǘ?jí)索引的葉子節(jié)點(diǎn)存放的數(shù)據(jù)是主鍵值),這樣二級(jí)索引占用的空間也就越小。
4. 防止索引失效;
發(fā)生索引失效的情況:
1. 當(dāng)我們使用左或者左右模糊匹配的時(shí)候,也就是?like %xx?或者?like %xx%這兩種方式都會(huì)造成索引失效;因?yàn)樗饕?B+ 樹(shù)是按照「索引值」有序排列存儲(chǔ)的,只能根據(jù)前綴進(jìn)行比較。
2. 當(dāng)我們?cè)诓樵儣l件中對(duì)索引列做了計(jì)算、函數(shù)、類型轉(zhuǎn)換操作,這些情況下都會(huì)造成索引失效;因?yàn)樗饕4娴氖撬饕侄蔚脑贾担皇墙?jīng)過(guò)函數(shù)計(jì)算后的值,自然就沒(méi)辦法走索引了。
3. 聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配,否則就會(huì)導(dǎo)致索引失效。原因是,在聯(lián)合索引的情況下,數(shù)據(jù)是按照索引第一列排序,第一列數(shù)據(jù)相同時(shí)才會(huì)按照第二列排序。
4. 在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會(huì)失效。這是因?yàn)?OR 的含義就是兩個(gè)只要滿足一個(gè)即可,因此只有一個(gè)條件列是索引列是沒(méi)有意義的,只要有條件列不是索引列,就會(huì)進(jìn)行全表掃描。
常見(jiàn)掃描類型的執(zhí)行效率從低到高的順序?yàn)?/b>:
1. All(全表掃描);
2. index(全索引掃描);
3. range(索引范圍掃描);
4. ref(非唯一索引掃描);
5. eq_ref(唯一索引掃描);
6. const(結(jié)果只有一條的主鍵或唯一索引掃描)。
9. 數(shù)據(jù)頁(yè)結(jié)構(gòu)
InnoDB 的數(shù)據(jù)是按「數(shù)據(jù)頁(yè)」為單位來(lái)讀寫(xiě)的,也就是說(shuō),當(dāng)需要讀一條記錄的時(shí)候,并不是將這個(gè)記錄本身從磁盤讀出來(lái),而是以頁(yè)為單位,將其整體讀入內(nèi)存。
數(shù)據(jù)庫(kù)的 I/O 操作的最小單位是頁(yè),InnoDB 數(shù)據(jù)頁(yè)的默認(rèn)大小是 16KB,意味著數(shù)據(jù)庫(kù)每次讀寫(xiě)都是以 16KB 為單位的,一次最少?gòu)拇疟P中讀取 16K 的內(nèi)容到內(nèi)存中,一次最少把內(nèi)存中的 16K 內(nèi)容刷新到磁盤中。



數(shù)據(jù)頁(yè)中的記錄按照「主鍵」順序組成單向鏈表,單向鏈表的特點(diǎn)就是插入、刪除非常方便,但是檢索效率不高,最差的情況下需要遍歷鏈表上的所有節(jié)點(diǎn)才能完成檢索。
因此,數(shù)據(jù)頁(yè)中有一個(gè)頁(yè)目錄,起到記錄的索引作用
頁(yè)目錄與記錄的關(guān)系如下圖

頁(yè)目錄創(chuàng)建的過(guò)程如下:
將所有的記錄劃分成幾個(gè)組,這些記錄包括最小記錄和最大記錄,但不包括標(biāo)記為“已刪除”的記錄;
每個(gè)記錄組的最后一條記錄就是組內(nèi)最大的那條記錄,并且最后一條記錄的頭信息中會(huì)存儲(chǔ)該組一共有多少條記錄,作為 n_owned 字段(上圖中粉紅色字段)
頁(yè)目錄用來(lái)存儲(chǔ)每組最后一條記錄的地址偏移量,這些地址偏移量會(huì)按照先后順序存儲(chǔ)起來(lái),每組的地址偏移量也被稱之為槽(slot),每個(gè)槽相當(dāng)于指針指向了不同組的最后一個(gè)記錄。
頁(yè)目錄就是由多個(gè)槽組成的,槽相當(dāng)于分組記錄的索引。然后,因?yàn)橛涗浭前凑铡钢麈I值」從小到大排序的,所以我們通過(guò)槽查找記錄時(shí),可以使用二分法快速定位要查詢的記錄在哪個(gè)槽(哪個(gè)記錄分組),定位到槽后,再遍歷槽內(nèi)的所有記錄,找到對(duì)應(yīng)的記錄,無(wú)需從最小記錄開(kāi)始遍歷整個(gè)頁(yè)中的記錄鏈表。
以上面那張圖舉個(gè)例子,5 個(gè)槽的編號(hào)分別為 0,1,2,3,4,我想查找主鍵為 11 的用戶記錄:
1. 先二分得出槽中間位是 (0+4)/2=2 ,2號(hào)槽里最大的記錄為 8。因?yàn)?11 > 8,所以需要從 2 號(hào)槽后繼續(xù)搜索記錄;
2. 再使用二分搜索出 2 號(hào)和 4 槽的中間位是 (2+4)/2= 3,3 號(hào)槽里最大的記錄為 12。因?yàn)?11 < 12,所以主鍵為 11 的記錄在 3 號(hào)槽里;
3. 這里有個(gè)問(wèn)題,「槽對(duì)應(yīng)的值都是這個(gè)組的主鍵最大的記錄,如何找到組里最小的記錄」?比如槽 3 對(duì)應(yīng)最大主鍵是 12 的記錄,那如何找到最小記錄 9。解決辦法是:通過(guò)槽 3 找到 槽 2 對(duì)應(yīng)的記錄,也就是主鍵為 8 的記錄。主鍵為 8 的記錄的下一條記錄就是槽 3 當(dāng)中主鍵最小的 9 記錄,然后開(kāi)始向下搜索 2 次,定位到主鍵為 11 的記錄,取出該條記錄的信息即為我們想要查找的內(nèi)容。
InnoDB 對(duì)每個(gè)分組中的記錄條數(shù)都是有規(guī)定的,槽內(nèi)的記錄就只有幾條:
1. 第一個(gè)分組中的記錄只能有 1 條記錄;
2. 最后一個(gè)分組中的記錄條數(shù)范圍只能在 1-8 條之間;
3. 剩下的分組中記錄條數(shù)范圍只能在 4-8 條之間。
10. B + Tree 結(jié)構(gòu)

通過(guò)上圖,我們看出 B+ 樹(shù)的特點(diǎn):
1. 只有葉子節(jié)點(diǎn)(最底層的節(jié)點(diǎn))才存放了數(shù)據(jù),非葉子節(jié)點(diǎn)(其他上層節(jié))僅用來(lái)存放目錄項(xiàng)作為索引。
2. 非葉子節(jié)點(diǎn)分為不同層次,通過(guò)分層來(lái)降低每一層的搜索量;
3. 所有節(jié)點(diǎn)按照索引鍵大小排序,構(gòu)成一個(gè)雙向鏈表,便于范圍查詢;
我們?cè)倏纯?B+ 樹(shù)如何實(shí)現(xiàn)快速查找主鍵為 6 的記錄,以上圖為例子:
1. 從根節(jié)點(diǎn)開(kāi)始,通過(guò)二分法快速定位到符合頁(yè)內(nèi)范圍包含查詢值的頁(yè),因?yàn)椴樵兊闹麈I值為 6,在[1, 7)范圍之間,所以到頁(yè) 30 中查找更詳細(xì)的目錄項(xiàng);
2. 在非葉子節(jié)點(diǎn)(頁(yè)30)中,繼續(xù)通過(guò)二分法快速定位到符合頁(yè)內(nèi)范圍包含查詢值的頁(yè),主鍵值大于 5,所以就到葉子節(jié)點(diǎn)(頁(yè)16)查找記錄;
3. 接著,在葉子節(jié)點(diǎn)(頁(yè)16)中,通過(guò)槽查找記錄時(shí),使用二分法快速定位要查詢的記錄在哪個(gè)槽(哪個(gè)記錄分組),定位到槽后,再遍歷槽內(nèi)的所有記錄,找到主鍵為 6 的記錄。
可以看到,在定位記錄所在哪一個(gè)頁(yè)時(shí),也是通過(guò)二分法快速定位到包含該記錄的頁(yè)。定位到該頁(yè)后,又會(huì)在該頁(yè)內(nèi)進(jìn)行二分法快速定位記錄所在的分組(槽號(hào)),最后在分組內(nèi)進(jìn)行遍歷查找
11. InnoDB數(shù)據(jù)查找
InnoDB 的數(shù)據(jù)是按「數(shù)據(jù)頁(yè)」為單位來(lái)讀寫(xiě)的,默認(rèn)數(shù)據(jù)頁(yè)大小為 16 KB。每個(gè)數(shù)據(jù)頁(yè)之間通過(guò)雙向鏈表的形式組織起來(lái),物理上不連續(xù),但是邏輯上連續(xù)。
數(shù)據(jù)頁(yè)內(nèi)包含用戶記錄,每個(gè)記錄之間用單向鏈表的方式組織起來(lái),為了加快在數(shù)據(jù)頁(yè)內(nèi)高效查詢記錄,設(shè)計(jì)了一個(gè)頁(yè)目錄,頁(yè)目錄存儲(chǔ)各個(gè)槽(分組),且主鍵值是有序的,于是可以通過(guò)二分查找法的方式進(jìn)行檢索從而提高效率。
為了高效查詢記錄所在的數(shù)據(jù)頁(yè),InnoDB 采用 b+ 樹(shù)作為索引,每個(gè)節(jié)點(diǎn)都是一個(gè)數(shù)據(jù)頁(yè)。
如果葉子節(jié)點(diǎn)存儲(chǔ)的是實(shí)際數(shù)據(jù)的就是聚簇索引,一個(gè)表只能有一個(gè)聚簇索引;如果葉子節(jié)點(diǎn)存儲(chǔ)的不是實(shí)際數(shù)據(jù),而是主鍵值則就是二級(jí)索引,一個(gè)表中可以有多個(gè)二級(jí)索引。
在使用二級(jí)索引進(jìn)行查找數(shù)據(jù)時(shí),如果查詢的數(shù)據(jù)能在二級(jí)索引找到,那么就是「索引覆蓋」操作,如果查詢的數(shù)據(jù)不在二級(jí)索引里,就需要先在二級(jí)索引找到主鍵值,需要去聚簇索引中獲得數(shù)據(jù)行,這個(gè)過(guò)程就叫作「回表」。
12. 為什么MySQL采用B+樹(shù)作為索引?
要設(shè)計(jì)一個(gè) MySQL 的索引數(shù)據(jù)結(jié)構(gòu),不僅僅考慮數(shù)據(jù)結(jié)構(gòu)增刪改的時(shí)間復(fù)雜度,更重要的是要考慮磁盤 I/0 的操作次數(shù)。因?yàn)樗饕陀涗浂际谴娣旁谟脖P,硬盤是一個(gè)非常慢的存儲(chǔ)設(shè)備,我們?cè)诓樵償?shù)據(jù)的時(shí)候,最好能在盡可能少的磁盤 I/0 的操作次數(shù)內(nèi)完成。
二分查找樹(shù)雖然是一個(gè)天然的二分結(jié)構(gòu),能很好的利用二分查找快速定位數(shù)據(jù),但是它存在一種極端的情況,每當(dāng)插入的元素都是樹(shù)內(nèi)最大的元素,就會(huì)導(dǎo)致二分查找樹(shù)退化成一個(gè)鏈表,此時(shí)查詢復(fù)雜度就會(huì)從 O(logn)降低為 O(n)。
為了解決二分查找樹(shù)退化成鏈表的問(wèn)題,就出現(xiàn)了自平衡二叉樹(shù),保證了查詢操作的時(shí)間復(fù)雜度就會(huì)一直維持在 O(logn) 。但是它本質(zhì)上還是一個(gè)二叉樹(shù),每個(gè)節(jié)點(diǎn)只能有 2 個(gè)子節(jié)點(diǎn),隨著元素的增多,樹(shù)的高度會(huì)越來(lái)越高。
而樹(shù)的高度決定于磁盤 I/O 操作的次數(shù),因?yàn)闃?shù)是存儲(chǔ)在磁盤中的,訪問(wèn)每個(gè)節(jié)點(diǎn),都對(duì)應(yīng)一次磁盤 I/O 操作,也就是說(shuō)樹(shù)的高度就等于每次查詢數(shù)據(jù)時(shí)磁盤 IO 操作的次數(shù),所以樹(shù)的高度越高,就會(huì)影響查詢性能。
B 樹(shù)和 B+ 都是通過(guò)多叉樹(shù)的方式,會(huì)將樹(shù)的高度變矮,所以這兩個(gè)數(shù)據(jù)結(jié)構(gòu)非常適合檢索存于磁盤中的數(shù)據(jù)。
但是 MySQL 默認(rèn)的存儲(chǔ)引擎 InnoDB 采用的是 B+ 作為索引的數(shù)據(jù)結(jié)構(gòu),原因有:
B+ 樹(shù)的非葉子節(jié)點(diǎn)不存放實(shí)際的記錄數(shù)據(jù),僅存放索引,因此數(shù)據(jù)量相同的情況下,相比存儲(chǔ)即存索引又存記錄的 B 樹(shù),B+樹(shù)的非葉子節(jié)點(diǎn)可以存放更多的索引,因此 B+ 樹(shù)可以比 B 樹(shù)更「矮胖」,查詢底層節(jié)點(diǎn)的磁盤 I/O次數(shù)會(huì)更少。
B+ 樹(shù)有大量的冗余節(jié)點(diǎn)(所有非葉子節(jié)點(diǎn)都是冗余索引),這些冗余索引讓 B+ 樹(shù)在插入、刪除的效率都更高,比如刪除根節(jié)點(diǎn)的時(shí)候,不會(huì)像 B 樹(shù)那樣會(huì)發(fā)生復(fù)雜的樹(shù)的變化;
B+ 樹(shù)葉子節(jié)點(diǎn)之間用鏈表連接了起來(lái),有利于范圍查詢,而 B 樹(shù)要實(shí)現(xiàn)范圍查詢,因此只能通過(guò)樹(shù)的遍歷來(lái)完成范圍查詢,這會(huì)涉及多個(gè)節(jié)點(diǎn)的磁盤 I/O 操作,范圍查詢效率不如 B+ 樹(shù)。
13.?事務(wù)有哪些特性?
原子性(Atomicity):一個(gè)事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié),而且事務(wù)在執(zhí)行過(guò)程中發(fā)生錯(cuò)誤,會(huì)被回滾到事務(wù)開(kāi)始前的狀態(tài),就像這個(gè)事務(wù)從來(lái)沒(méi)有執(zhí)行過(guò)一樣,就好比買一件商品,購(gòu)買成功時(shí),則給商家付了錢,商品到手;購(gòu)買失敗時(shí),則商品在商家手中,消費(fèi)者的錢也沒(méi)花出去。
一致性(Consistency):是指事務(wù)操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫(kù)保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉(zhuǎn)賬 200 元,分為兩個(gè)步驟,從 A 的賬戶扣除 200 元和對(duì) B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結(jié)果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會(huì)出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。
隔離性(Isolation):數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫(xiě)和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致,因?yàn)槎鄠€(gè)事務(wù)同時(shí)使用相同的數(shù)據(jù)時(shí),不會(huì)相互干擾,每個(gè)事務(wù)都有一個(gè)完整的數(shù)據(jù)空間,對(duì)其他并發(fā)事務(wù)是隔離的。也就是說(shuō),消費(fèi)者購(gòu)買商品這個(gè)事務(wù),是不影響其他消費(fèi)者購(gòu)買的。
持久性(Durability):事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。
InnoDB 引擎通過(guò)什么技術(shù)來(lái)保證事務(wù)的這四個(gè)特性的呢?
1. 持久性是通過(guò) redo log (重做日志)來(lái)保證的;
2. 原子性是通過(guò) undo log(回滾日志) 來(lái)保證的;
3. 隔離性是通過(guò) MVCC(多版本并發(fā)控制) 或鎖機(jī)制來(lái)保證的;
4. 一致性則是通過(guò)持久性+原子性+隔離性來(lái)保證;
14. 并行事務(wù)會(huì)引發(fā)什么問(wèn)題?
在同時(shí)處理多個(gè)事務(wù)的時(shí)候,就可能出現(xiàn)臟讀(dirty read)、不可重復(fù)讀(non-repeatable read)、幻讀(phantom read)的問(wèn)題。
臟讀
如果一個(gè)事務(wù)「讀到」了另一個(gè)「未提交事務(wù)修改過(guò)的數(shù)據(jù)」,就意味著發(fā)生了「臟讀」現(xiàn)象。
不可重復(fù)讀
在一個(gè)事務(wù)內(nèi)多次讀取同一個(gè)數(shù)據(jù),如果出現(xiàn)前后兩次讀到的數(shù)據(jù)不一樣的情況,就意味著發(fā)生了「不可重復(fù)讀」現(xiàn)象。
幻讀
在一個(gè)事務(wù)內(nèi)多次查詢某個(gè)符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
15.?事務(wù)的隔離級(jí)別有哪些?
SQL 標(biāo)準(zhǔn)提出了四種隔離級(jí)別來(lái)規(guī)避這些現(xiàn)象,隔離級(jí)別越高,性能效率就越低,這四個(gè)隔離級(jí)別如下:
讀未提交(read uncommitted),指一個(gè)事務(wù)還沒(méi)提交時(shí),它做的變更就能被其他事務(wù)看到;
讀提交(read committed),指一個(gè)事務(wù)提交之后,它做的變更才能被其他事務(wù)看到;
可重復(fù)讀(repeatable read),指一個(gè)事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的,MySQL InnoDB 引擎的默認(rèn)隔離級(jí)別;
串行化(serializable?);會(huì)對(duì)記錄加上讀寫(xiě)鎖,在多個(gè)事務(wù)對(duì)這條記錄進(jìn)行讀寫(xiě)操作時(shí),如果發(fā)生了讀寫(xiě)沖突的時(shí)候,后訪問(wèn)的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行;

MySQL 在「可重復(fù)讀」隔離級(jí)別下,可以很大程度上避免幻讀現(xiàn)象的發(fā)生(注意是很大程度避免,并不是徹底避免),所以 MySQL 并不會(huì)使用「串行化」隔離級(jí)別來(lái)避免幻讀現(xiàn)象的發(fā)生,因?yàn)槭褂谩复谢垢綦x級(jí)別會(huì)影響性能。
MySQL InnoDB 引擎的默認(rèn)隔離級(jí)別雖然是「可重復(fù)讀」,但是它很大程度上避免幻讀現(xiàn)象(并不是完全解決了),?解決的方案有兩種:
1. 針對(duì)快照讀(普通 select 語(yǔ)句),是通過(guò) MVCC 方式解決了幻讀,因?yàn)榭芍貜?fù)讀隔離級(jí)別下,事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的,即使中途有其他事務(wù)插入了一條數(shù)據(jù),是查詢不出來(lái)這條數(shù)據(jù)的,所以就很好了避免幻讀問(wèn)題。
2. 針對(duì)當(dāng)前讀(select ... for update 等語(yǔ)句),是通過(guò) next-key lock(記錄鎖+間隙鎖)方式解決了幻讀,因?yàn)楫?dāng)執(zhí)行 select ... for update 語(yǔ)句的時(shí)候,會(huì)加上 next-key lock,如果有其他事務(wù)在 next-key lock 鎖范圍內(nèi)插入了一條記錄,那么這個(gè)插入語(yǔ)句就會(huì)被阻塞,無(wú)法成功插入,所以就很好了避免幻讀問(wèn)題。
這四種隔離級(jí)別具體是如何實(shí)現(xiàn)的呢?
1. 對(duì)于「讀未提交」隔離級(jí)別的事務(wù)來(lái)說(shuō),因?yàn)榭梢宰x到未提交事務(wù)修改的數(shù)據(jù),所以直接讀取最新的數(shù)據(jù)就好了;
2. 對(duì)于「串行化」隔離級(jí)別的事務(wù)來(lái)說(shuō),通過(guò)加讀寫(xiě)鎖的方式來(lái)避免并行訪問(wèn);
3. 對(duì)于「讀提交」和「可重復(fù)讀」隔離級(jí)別的事務(wù)來(lái)說(shuō),它們是通過(guò)?Read View?來(lái)實(shí)現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時(shí)機(jī)不同,大家可以把 Read View 理解成一個(gè)數(shù)據(jù)快照,就像相機(jī)拍照那樣,定格某一時(shí)刻的風(fēng)景?!缸x提交」隔離級(jí)別是在「每個(gè)語(yǔ)句執(zhí)行前」都會(huì)重新生成一個(gè) Read View,而「可重復(fù)讀」隔離級(jí)別是「啟動(dòng)事務(wù)時(shí)」生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View。
16.?Read View 在 MVCC 里如何工作的?

Read View 有四個(gè)重要的字段:
1. m_ids :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」的事務(wù) id 列表,注意是一個(gè)列表,“活躍事務(wù)”指的就是,啟動(dòng)了但還沒(méi)提交的事務(wù)。
2. min_trx_id :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」中事務(wù)?id 最小的事務(wù),也就是 m_ids 的最小值。
3. max_trx_id :這個(gè)并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時(shí)當(dāng)前數(shù)據(jù)庫(kù)中應(yīng)該給下一個(gè)事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值 + 1;
4. creator_trx_id :指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。

對(duì)于使用 InnoDB 存儲(chǔ)引擎的數(shù)據(jù)庫(kù)表,它的聚簇索引記錄中都包含下面兩個(gè)隱藏列:
1. trx_id,當(dāng)一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),就會(huì)把該事務(wù)的事務(wù) id 記錄在 trx_id 隱藏列里;
2. roll_pointer,每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊版本的記錄寫(xiě)入到 undo 日志中,然后這個(gè)隱藏列是個(gè)指針,指向每一個(gè)舊版本記錄,于是就可以通過(guò)它找到修改前的記錄。
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:

一個(gè)事務(wù)去訪問(wèn)記錄的時(shí)候,除了自己的更新記錄總是可見(jiàn)之外,還有這幾種情況:
1. 如果記錄的 trx_id 值小于 Read View 中的?min_trx_id?值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View?前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對(duì)當(dāng)前事務(wù)可見(jiàn)。
2. 如果記錄的 trx_id 值大于等于 Read View 中的?max_trx_id?值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View?后才啟動(dòng)的事務(wù)生成的,所以該版本的記錄對(duì)當(dāng)前事務(wù)不可見(jiàn)。
3. 如果記錄的 trx_id 值在 Read View 的?min_trx_id?和?max_trx_id?之間,需要判斷 trx_id 是否在 m_ids 列表中:
4. 如果記錄的 trx_id?在?m_ids?列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒(méi)提交事務(wù)),所以該版本的記錄對(duì)當(dāng)前事務(wù)不可見(jiàn)。
5. 如果記錄的 trx_id?不在?m_ids列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對(duì)當(dāng)前事務(wù)可見(jiàn)。
這種通過(guò)「版本鏈」來(lái)控制并發(fā)事務(wù)訪問(wèn)同一個(gè)記錄時(shí)的行為就叫 MVCC(多版本并發(fā)控制)。
通過(guò)「事務(wù)的 Read View 里的字段」和「記錄中的兩個(gè)隱藏列」的比對(duì),來(lái)控制并發(fā)事務(wù)訪問(wèn)同一個(gè)記錄時(shí)的行為,這就叫 MVCC(多版本并發(fā)控制)
17. 行級(jí)鎖
共享鎖(S鎖)滿足讀讀共享,讀寫(xiě)互斥。獨(dú)占鎖(X鎖)滿足寫(xiě)寫(xiě)互斥、讀寫(xiě)互斥。

行級(jí)鎖的類型主要有三類:
1. Record Lock,記錄鎖,也就是僅僅把一條記錄鎖上;
2. Gap Lock,間隙鎖,鎖定一個(gè)范圍,但是不包含記錄本身;
3. Next-Key Lock:Record Lock + Gap Lock 的組合,鎖定一個(gè)范圍,并且鎖定記錄本身。
Record Lock
Record Lock 稱為記錄鎖,鎖住的是一條記錄。而且記錄鎖是有 S 鎖和 X 鎖之分的:
當(dāng)一個(gè)事務(wù)對(duì)一條記錄加了 S 型記錄鎖后,其他事務(wù)也可以繼續(xù)對(duì)該記錄加 S 型記錄鎖(S 型與 S 鎖兼容),但是不可以對(duì)該記錄加 X 型記錄鎖(S 型與 X 鎖不兼容);
當(dāng)一個(gè)事務(wù)對(duì)一條記錄加了 X 型記錄鎖后,其他事務(wù)既不可以對(duì)該記錄加 S 型記錄鎖(S 型與 X 鎖不兼容),也不可以對(duì)該記錄加 X 型記錄鎖(X 型與 X 鎖不兼容)。
Gap Lock
Gap Lock 稱為間隙鎖,只存在于可重復(fù)讀隔離級(jí)別,目的是為了解決可重復(fù)讀隔離級(jí)別下幻讀的現(xiàn)象。
間隙鎖雖然存在 X 型間隙鎖和 S 型間隙鎖,但是并沒(méi)有什么區(qū)別,間隙鎖之間是兼容的,即兩個(gè)事務(wù)可以同時(shí)持有包含共同間隙范圍的間隙鎖,并不存在互斥關(guān)系,因?yàn)殚g隙鎖的目的是防止插入幻影記錄而提出的。
Next-Key Lock
Next-Key Lock 稱為臨鍵鎖,是 Record Lock + Gap Lock 的組合,鎖定一個(gè)范圍,并且鎖定記錄本身。
假設(shè),表中有一個(gè)范圍 id 為(3,5] 的 next-key lock,那么其他事務(wù)即不能插入 id = 4 記錄,也不能修改 id = 5 這條記錄。

next-key lock 是包含間隙鎖+記錄鎖的,如果一個(gè)事務(wù)獲取了 X 型的 next-key lock,那么另外一個(gè)事務(wù)在獲取相同范圍的 X 型的 next-key lock 時(shí),是會(huì)被阻塞的。
插入意向鎖
一個(gè)事務(wù)在插入一條記錄的時(shí)候,需要判斷插入位置是否已被其他事務(wù)加了間隙鎖(next-key lock 也包含間隙鎖)。
如果有的話,插入操作就會(huì)發(fā)生阻塞,直到擁有間隙鎖的那個(gè)事務(wù)提交為止(釋放間隙鎖的時(shí)刻
插入意向鎖名字雖然有意向鎖,但是它并不是意向鎖,它是一種特殊的間隙鎖,屬于行級(jí)別鎖。
18. 什么 SQL 語(yǔ)句會(huì)加行級(jí)鎖?
普通的 select 語(yǔ)句是不會(huì)對(duì)記錄加鎖的(除了串行化隔離級(jí)別),因?yàn)樗鼘儆诳煺兆x,是通過(guò) MVCC(多版本并發(fā)控制)實(shí)現(xiàn)的。update 和 delete 操作都會(huì)加行級(jí)鎖,且鎖的類型都是獨(dú)占鎖(X型鎖)。
19.?MySQL 是怎么加行級(jí)鎖的?
行級(jí)鎖加鎖規(guī)則比較復(fù)雜,不同的場(chǎng)景,加鎖的形式是不同的。
加鎖的對(duì)象是索引,加鎖的基本單位是 next-key lock,它是由記錄鎖和間隙鎖組合而成的,next-key lock 是前開(kāi)后閉區(qū)間,而間隙鎖是前開(kāi)后開(kāi)區(qū)間。
但是,next-key lock 在一些場(chǎng)景下會(huì)退化成記錄鎖或間隙鎖。
那到底是什么場(chǎng)景呢?總結(jié)一句,在能使用記錄鎖或者間隙鎖就能避免幻讀現(xiàn)象的場(chǎng)景下, next-key lock 就會(huì)退化成記錄鎖或間隙鎖。
20. 如何避免死鎖
死鎖的四個(gè)必要條件:互斥、占有且等待、不可強(qiáng)占用、循環(huán)等待。只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,但是只要破壞任意一個(gè)條件就死鎖就不會(huì)成立。
在數(shù)據(jù)庫(kù)層面,有兩種策略通過(guò)「打破循環(huán)等待條件」來(lái)解除死鎖狀態(tài):
設(shè)置事務(wù)等待鎖的超時(shí)時(shí)間。當(dāng)一個(gè)事務(wù)的等待時(shí)間超過(guò)該值后,就對(duì)這個(gè)事務(wù)進(jìn)行回滾,于是鎖就釋放了,另一個(gè)事務(wù)就可以繼續(xù)執(zhí)行了。在 InnoDB 中,參數(shù)?innodb_lock_wait_timeout?是用來(lái)設(shè)置超時(shí)時(shí)間的,默認(rèn)值時(shí) 50 秒。
當(dāng)發(fā)生超時(shí)后,就出現(xiàn)下面這個(gè)提示:
開(kāi)啟主動(dòng)死鎖檢測(cè)。主動(dòng)死鎖檢測(cè)在發(fā)現(xiàn)死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù),讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù)?innodb_deadlock_detect?設(shè)置為 on,表示開(kāi)啟這個(gè)邏輯,默認(rèn)就開(kāi)啟。
上面這個(gè)兩種策略是「當(dāng)有死鎖發(fā)生時(shí)」的避免方式。
我們可以回歸業(yè)務(wù)的角度來(lái)預(yù)防死鎖,對(duì)訂單做冪等性校驗(yàn)的目的是為了保證不會(huì)出現(xiàn)重復(fù)的訂單,那我們可以直接將 order_no 字段設(shè)置為唯一索引列,利用它的唯一性來(lái)保證訂單表不會(huì)出現(xiàn)重復(fù)的訂單,不過(guò)有一點(diǎn)不好的地方就是在我們插入一個(gè)已經(jīng)存在的訂單記錄時(shí)就會(huì)拋出異常。