一篇帶給你 MySQL 不完全入門(mén)指南
由于 MySQL 的整個(gè)體系太過(guò)于龐大,文章的篇幅有限,不能夠完全的覆蓋所有的方面。所以我會(huì)盡可能的從更加貼進(jìn)我們?nèi)粘J褂玫姆绞絹?lái)進(jìn)行解釋。
小白眼中的 MySQL
首先,對(duì)于我們來(lái)說(shuō),MySQL 是個(gè)啥?我們從一個(gè)最簡(jiǎn)單的例子來(lái)回顧一下。
這可能就是最開(kāi)始大家認(rèn)知中的 MySQL。那 MySQL 中是怎么處理這個(gè)查詢(xún)語(yǔ)句的呢?換句話說(shuō),它是如何感知到這串字符串是一個(gè)查詢(xún)語(yǔ)句的?它是如何感知到該去哪張表中取數(shù)據(jù)?它是如何感知到該如何取數(shù)據(jù)?
到目前為止,都不知道。接下來(lái)我們一步一補(bǔ)來(lái)進(jìn)行解析。
連接池
首先,要去 MySQL 執(zhí)行命令,肯定是需要連接上 MySQL 服務(wù)器的,就像我們通過(guò)「用戶(hù)名」和「密碼」登陸網(wǎng)站一樣。所以,我們首先要認(rèn)識(shí)的就是連接池。
這種池化技術(shù)的作用很明顯,復(fù)用連接,避免頻繁的銷(xiāo)毀、創(chuàng)建線程所帶來(lái)的開(kāi)銷(xiāo)。除此之外,在這一層還可以根據(jù)你的賬號(hào)密碼對(duì)用戶(hù)的合法性、用戶(hù)的權(quán)限進(jìn)行校驗(yàn)。
每一個(gè)連接都對(duì)應(yīng)一個(gè)線程,「服務(wù)器」 和 「MySQL」 都一樣,服務(wù)器的一個(gè)線程從服務(wù)器的連接池中取出一個(gè)連接,發(fā)起查詢(xún)語(yǔ)句。MySQL 服務(wù)器的線程從連接池中取出一個(gè)線程,繼續(xù)后續(xù)的流程。
那么后面的流程是啥呢?當(dāng)然是分析 SQL 語(yǔ)句了。
分析器
很明顯,MySQL 肯定得知道這個(gè) SQL 是不是個(gè)合法的 SQL 語(yǔ)句,以及 SQL 語(yǔ)句到底要干啥?
就好像有個(gè)哥們瘋狂的敲你家門(mén),門(mén)打開(kāi)了,下一步是干嘛?肯定得問(wèn)他是誰(shuí)?來(lái)干嘛?
所以,下一步就是要將 SQL 進(jìn)行解析。解析完成之后,我們就知道當(dāng)前的 SQL 是否符合語(yǔ)法,它到底要干嘛,是要查詢(xún)數(shù)據(jù)?還是要更新數(shù)據(jù)?還是要?jiǎng)h除數(shù)據(jù)?
很簡(jiǎn)單,我們?nèi)庋勰苣苊黠@看出來(lái)一條 SQL 語(yǔ)句是要干嘛。但電腦不是人腦,我們得讓電腦也能看懂這條 SQL 語(yǔ)句,才能幫我們?nèi)プ龊竺娴氖隆?/p>
知道了這個(gè) SQL 語(yǔ)言要干嘛之后,是不是就可以開(kāi)始執(zhí)行操作了呢?
并不是
優(yōu)化器
MySQL 除了要知道這條 SQL 要干嘛,在執(zhí)行之前,還得決定怎么干,怎么干是最優(yōu)解。
還是剛剛那個(gè)例子,隔壁的哥們敲開(kāi)了你家的門(mén),說(shuō)哥們兒,我家里這停水了,想找你施(白)舍(漂) 24 瓶礦泉水。我們暫且不去討論,他為什么需要 24 瓶礦泉水。
我們要討論的是,我們要怎么把這 24 瓶礦泉水拿給他。因?yàn)槟銊倓傁肫饋?lái),礦泉水在之前被你一頓操作全扔柜子了。
你是要每次拿個(gè) 4 瓶,跑 6 趟呢?還是找個(gè)箱子,把 24 瓶裝滿再搬出去給他。
這差不多就是優(yōu)化器要做的事情,優(yōu)化器會(huì)分析你的 SQL,找出最優(yōu)解。
再舉個(gè)正經(jīng)的例子,假設(shè) SELECTnameFROMstudentwhereid= 1 語(yǔ)句執(zhí)行時(shí),數(shù)據(jù)庫(kù)里有 1W 條數(shù)據(jù),此時(shí)有兩種方案:
- 查出所有列的 name 值,然后再遍歷對(duì)比,找到 id=1 的 name 值
- 直接找到 id=1 的數(shù)據(jù),然后再取 name 的值
用屁股想想都知道應(yīng)該選方案2.
找到了怎么做之后,接下來(lái)就需要落實(shí)到行動(dòng)上了。所以,接下來(lái)執(zhí)行器閃亮登場(chǎng)。
執(zhí)行器
執(zhí)行器會(huì)掉用底層存儲(chǔ)引擎的接口,來(lái)真正的執(zhí)行 SQL 語(yǔ)句,在這里的例子就是查詢(xún)操作。
至此,MySQL 這個(gè)黑盒子已經(jīng)被我們一步一步的揭開(kāi)了面紗。但是在揭開(kāi)最后一片面紗的時(shí)候,我們又發(fā)現(xiàn)了新的黑盒子。那就是存儲(chǔ)引擎。
我們到目前為止,就只知道它的名字,至于其如何存儲(chǔ)數(shù)據(jù),如何查詢(xún)數(shù)據(jù),一概不知。
存儲(chǔ)引擎
MySQL 的存儲(chǔ)引擎有很多的種類(lèi),分別適用于不同的場(chǎng)景。大家可以這么理解,存儲(chǔ)引擎就是一個(gè)執(zhí)行數(shù)據(jù)操作的接口(Interface),而底層的具體實(shí)現(xiàn)有很多類(lèi)。
InnoDB、MyISAM、Memory、CSV、Archive、Blackhole、Merge、Federated、Example
用的最廣泛的,就是 InnoDB 了。打從 MySQL 5.5 之后,InnoDB 就是 MySQL 默認(rèn)的存儲(chǔ)引擎了。
所以,存儲(chǔ)引擎其實(shí)并不是什么高大上的東西,跟什么大學(xué)拿去交作業(yè)的圖書(shū)館管理系統(tǒng)區(qū)別,就差了億點(diǎn)而已。
或者,我再舉個(gè)例子。我們往我們電腦上的文件中寫(xiě)入數(shù)據(jù),此時(shí)由于 OS 的優(yōu)化,數(shù)據(jù)并不會(huì)直接寫(xiě)入磁盤(pán),因?yàn)?I/O 操作相當(dāng)?shù)陌嘿F。數(shù)據(jù)會(huì)先進(jìn)入到 OS 的 Cache 中,由 OS 之后刷入磁盤(pán)。而 MySQL 在整個(gè)的邏輯結(jié)構(gòu)上,跟計(jì)算機(jī)寫(xiě)文件差不多。
從上面的例子看出,存儲(chǔ)引擎可以分為兩部分:
- 內(nèi)存
- 磁盤(pán)
所以,從宏觀上來(lái)說(shuō),MySQL 就是把數(shù)據(jù)在緩存在內(nèi)存中,鼓搗鼓搗,然后在某些時(shí)候刷入磁盤(pán)中去,就這么回事。
接下來(lái),就讓我們深入存儲(chǔ)引擎 InnoDB 的底層原理中相當(dāng)重要的一部分——內(nèi)存架構(gòu)。
簡(jiǎn)單來(lái)說(shuō),InnoDB 的內(nèi)存由以下兩部分組成:
- Buffer Pool
- Log Buffer
從上面畫(huà)的圖就能夠看出,Buffer Pool 是 InnoDB 中非常重要的一部分,MySQL 之所以這么快其中一個(gè)重要的原因就是其數(shù)據(jù)都存在內(nèi)存中,而這個(gè)內(nèi)存就是 Buffer Pool。
Buffer Pool
一般來(lái)說(shuō),宿主機(jī)的 80% 的內(nèi)存都會(huì)分配給 Buffer Pool。這個(gè)很好理解,內(nèi)存越大,就能夠存下更多的數(shù)據(jù)。這樣一來(lái)更多的數(shù)據(jù)將可以直接在內(nèi)存中讀取,可以大大的提升操作效率。
那 Buffer Pool 中到底是如何存儲(chǔ)數(shù)據(jù)的呢?如果其底層的數(shù)據(jù)存儲(chǔ)不進(jìn)行特殊的設(shè)計(jì)、優(yōu)化,那么 InnoDB 在取數(shù)據(jù)的時(shí)候除了整個(gè)遍歷之外,沒(méi)有其他的捷徑。而如果那樣做,MySQL 也不會(huì)獲得今天這樣的地位。
頁(yè)
如果我們能想象一下,InnoDB 會(huì)如何組織內(nèi)存的數(shù)據(jù)。想象一下,圖書(shū)館的書(shū)是直接一本一本的攤在地上好找,還是按照類(lèi)目、名稱(chēng)進(jìn)行分類(lèi)、放到對(duì)應(yīng)的書(shū)架上、再進(jìn)行編號(hào)好找?結(jié)論自然不言而喻。Buffer Pool 也采用了同樣的數(shù)據(jù)整合措施。
InnoDB 將 Buffer Pool 分成了一張一張的頁(yè)(Pages),同時(shí)還有個(gè) Change Buffer(后面會(huì)詳細(xì)講,這里先知道就行)。分成一頁(yè)一頁(yè)的數(shù)據(jù)就能夠提升查詢(xún)效率嗎?那這個(gè)頁(yè)里面到底是個(gè)啥呢?
可以從上圖看到,頁(yè)和頁(yè)之間,實(shí)際上是有關(guān)聯(lián)的,他們通過(guò)雙向鏈表進(jìn)行連接,可以方便的從某一頁(yè)跳到下一頁(yè)。
那數(shù)據(jù)在頁(yè)中具體是如何存儲(chǔ)的呢?
User Records
當(dāng)然,光跳來(lái)跳去的并不能說(shuō)明任何問(wèn)題,我們還是揭開(kāi)頁(yè)(Pages)這個(gè)黑盒的面紗吧。
可以看到,主要就分為
- 上一頁(yè)指針
- 下一頁(yè)指針
- User Records
- 其余字段
為了方便理解,其余字段我后續(xù)會(huì)補(bǔ)充
上一頁(yè)指針、下一頁(yè)指針就不多贅述,就是一個(gè)指針。重點(diǎn)我們需要了解 User Records。
User Records 就是一行一行的數(shù)據(jù)**(Rows)最終存儲(chǔ)的地方,其中,行與行之間形成了單向鏈表**。
看了這個(gè)單向鏈表不知道你有沒(méi)有一個(gè)疑問(wèn)。
我們知道,在聚簇索引中,Key 實(shí)際上會(huì)按照 Primary Key 的順序來(lái)進(jìn)行排列。那在 User Records 中也會(huì)這樣嗎?我們插入一條新的數(shù)據(jù)到 User Records 中時(shí),是否也會(huì)按照 Primary Key 的順序來(lái)對(duì)已有的數(shù)據(jù)重排序?
如果每次插入數(shù)據(jù)都需要對(duì) User Records 中的數(shù)據(jù)進(jìn)行重排序,那么 MySQL 的江湖地位將再次不保。
雖然在圖中看起來(lái)是按照「主鍵」的順序存儲(chǔ)的,但實(shí)際上是按照數(shù)據(jù)的插入順序來(lái)存儲(chǔ)的,先到的數(shù)據(jù)會(huì)在前面,后到的數(shù)據(jù)會(huì)在后面,只是每個(gè) User Records 數(shù)據(jù)的指針指向的不是物理上的下一個(gè),而是邏輯上的下一個(gè)。
用圖來(lái)表示,大概如下:
可以理解為數(shù)組和鏈表的區(qū)別。
看到這,那么問(wèn)題來(lái)了,說(shuō)好的不遍歷呢?這不是打臉嗎?因?yàn)閺纳蠄D可以看出,我要找查找某個(gè)數(shù)據(jù)是否存在于當(dāng)前的頁(yè)(Pages)中,只能從頭開(kāi)始遍歷這個(gè)單向鏈表。
就這?還敢號(hào)稱(chēng)高性能?當(dāng)然,InnoDB 肯定不是這么搞的。這下就需要從「其余字段」中取出一部分字段了來(lái)解釋了。
Infimum 和 Supremum
分別代表當(dāng)前頁(yè)(Pages)中的最大和最小的記錄。
可以看到,有了 Infimum 和 Supremum,我們不需要再去遍歷 User Records 就能夠知道,要找的數(shù)據(jù)是否在當(dāng)前的頁(yè)中,大大的提升了效率。
但其實(shí)還是有問(wèn)題,比如我需要查詢(xún)的數(shù)據(jù)不在當(dāng)前頁(yè)中還好,那如果在呢?那是不是還是逃不了 O(N) 的鏈表遍歷呢?算不算治標(biāo)不治本?
這個(gè)時(shí)候,我們又需要從「其余字段」中抽一個(gè)概念出來(lái)了。
Page Directory
顧名思義,這玩意兒是個(gè)「目錄」,可以看下圖。
可以看到,每隔一段記錄就會(huì)在 Page Directory 中有個(gè)記錄,這個(gè)記錄是一個(gè)指向 User Records 中記錄的一個(gè)指針。
不知道這個(gè)設(shè)計(jì)有沒(méi)有讓你想起跳表(Skip List)。那這個(gè) Page Directory 中的目錄拿來(lái)干嘛呢?
有了 Page Directory,就可以對(duì)一頁(yè)中的數(shù)據(jù)進(jìn)行類(lèi)似于跳表的中的查詢(xún)。在 Page Directory 中找到對(duì)應(yīng)的「位置」之后,再根據(jù)指針跳到對(duì)應(yīng)的 User Records 上的單鏈表,進(jìn)行查詢(xún)。如此一來(lái)就避免了遍歷全部的數(shù)據(jù)。
上面提到的「位置」,其實(shí)有個(gè)專(zhuān)業(yè)的名詞叫「槽位(Slots)」。每一個(gè)槽位的數(shù)據(jù)都是一個(gè)指向了 User Records 某條記錄的指針。
當(dāng)我們新增每條數(shù)據(jù)的時(shí)候,就會(huì)同步的對(duì) Page Directory 中的槽位進(jìn)行維護(hù)。InnoDB 規(guī)定每隔 6 條記錄就會(huì)創(chuàng)建一個(gè) Slot。
了解到這里之后,關(guān)于如何高效地在 MySQL 查詢(xún)數(shù)據(jù)就已經(jīng)了解的差不多了。
想了解「其余字段」還有哪些、以及「頁(yè)」的完整面貌的,可以去看看我之前寫(xiě)的頁(yè)的文章 MySQL 頁(yè)完全指南——淺入深出頁(yè)的原理,再次就不再贅述。
索引
了解完頁(yè)之后,索引是什么就一目了然了。InnoDB 底層的存儲(chǔ)使用的數(shù)據(jù)結(jié)構(gòu)為 B+樹(shù),B樹(shù)的變種。MySQL 中有兩種索引,分別是聚簇索引和非聚簇索引,聽(tīng)著很高大上。
其實(shí)了解完「頁(yè)」的底層原理,要區(qū)分它們就變成的很簡(jiǎn)單了。
- 聚簇索引的葉子結(jié)點(diǎn)上,存儲(chǔ)的是「頁(yè)」
- **非聚簇索引(二級(jí)索引)**的葉子結(jié)點(diǎn)上,存儲(chǔ)的是「主鍵ID」。很多時(shí)候,我們都需要通過(guò)非聚簇索引拿到主鍵,再根據(jù)這個(gè)主鍵去「聚簇索引」中拿完整的數(shù)據(jù),這個(gè)過(guò)程還有一個(gè)很有意思的名字叫「回表」。
至于為什么底層數(shù)據(jù)結(jié)構(gòu)要用 B+樹(shù) 和 B樹(shù),大概是因?yàn)橐韵氯c(diǎn):
- B+樹(shù)能夠減少 I/O 的次數(shù)
- 查詢(xún)效率更加的穩(wěn)定
- 能夠更好的支持范圍查詢(xún)
詳細(xì)的原因可以參考之前寫(xiě)的 淺入淺出 MySQL 索引
更新數(shù)據(jù)
為什么下一步就是要看如何更新數(shù)據(jù)呢?因?yàn)樯鲜龅摹疙?yè)」的原理主要都是基于「查詢(xún)」的前提在講,看完了之后對(duì)查詢(xún)的過(guò)程應(yīng)該了然于胸了。接下來(lái)我們就來(lái)看看更新的時(shí)候會(huì)發(fā)生什么。
首先,如果我們插入了某條 id=100 的數(shù)據(jù),然后再去更新的話,這條數(shù)據(jù)是一定的在 Buffer Pool 的。這句話看似是廢話(我都寫(xiě)到數(shù)據(jù)庫(kù)了那肯定存在啊)
那我換個(gè)說(shuō)法,更新的時(shí)候,id=100 這條數(shù)據(jù)可能不在 Buffer Pool 中。為什么之前寫(xiě)入了 Buffer Pool,之后再來(lái)更新 Buffer Pool 中又沒(méi)有呢?
答案是內(nèi)存是有限的,我們不可能無(wú)限的向 Buffer Pool 中插入數(shù)據(jù)。熟悉 Redis 的知道,Redis 在運(yùn)行時(shí)會(huì)有「過(guò)期策略」,有以下三種:
- 定時(shí)過(guò)期
- 惰性過(guò)期
- 定期過(guò)期
而 Buffer Pool 同樣也是基于內(nèi)存,同樣也需要一個(gè)「過(guò)期策略」來(lái)清理掉一些不常被訪問(wèn)的數(shù)據(jù),來(lái)為新的數(shù)據(jù)、熱點(diǎn)數(shù)據(jù)騰出空間。
當(dāng)然,這里的清理掉,并不是刪除,而是將它們刷入磁盤(pán)
更新數(shù)據(jù)時(shí),如果發(fā)現(xiàn)對(duì)應(yīng)的數(shù)據(jù)不存在,就會(huì)將那個(gè)數(shù)據(jù)所在的頁(yè)加載到 Buffer Pool 中來(lái)。注意,這里并不是只加載 id=100 這一行,而是其所在的一整「頁(yè)」數(shù)據(jù)。
加載到 Buffer Pool 中之后,再對(duì) Buffer Pool 中的數(shù)據(jù)進(jìn)行更新。當(dāng)然,這個(gè)情況對(duì)我們開(kāi)發(fā)人員來(lái)說(shuō),是針對(duì)聚簇索引的。
還有另一種情況是針對(duì)「 非聚簇索引」 的。
Change Buffer
很簡(jiǎn)單,當(dāng)我們更新了某些字段之后,假設(shè)這些字段是組成非聚簇索引的字段,就會(huì)涉及到非聚簇索引的更新,但不巧的是該非聚簇索引所在的頁(yè)不在 Buffer Pool 中。按照之前的說(shuō)法,需要將對(duì)應(yīng)的頁(yè)(Pages)加載到 Buffer Pool 中來(lái)。
但是這里有一個(gè)很大的問(wèn)題,這個(gè)二級(jí)索引可能之后**根本不會(huì)被用到,**那這樣一來(lái),剛剛昂貴的 I/O 操作就被浪費(fèi)掉了。積少成多,如果每次涉及到更新二級(jí)索引發(fā)現(xiàn)在 Buffer Pool 中不存在,都去做 I/O 操作,那也是一個(gè)相當(dāng)大的開(kāi)銷(xiāo)。
所以,InnoDB 才設(shè)計(jì)了 Change Buffer。Change Buffer 就是專(zhuān)門(mén)用來(lái)存儲(chǔ)當(dāng)「非聚簇索引」所在的頁(yè)不在 Buffer Pool 時(shí)的更改的。
換句話說(shuō),當(dāng)對(duì)應(yīng)的非聚簇索引被修改并且對(duì)應(yīng)的頁(yè)(Pages)不在 Buffer Pool 中時(shí),會(huì)將其改動(dòng)暫存在 Change Buffer,等到其對(duì)應(yīng)的頁(yè)被其他的請(qǐng)求加載進(jìn) Buffer Pool 時(shí),就會(huì)將 Change Buffer 中暫存的數(shù)據(jù) 和 Buffer Pool 中的數(shù)據(jù)進(jìn)行合并。
當(dāng)然,Change Buffer 這個(gè)設(shè)計(jì)也不是沒(méi)有缺點(diǎn)。當(dāng) Change Buffer 中有很多的數(shù)據(jù)時(shí),全部合并到Buffer Pool可能會(huì)花上幾個(gè)小時(shí)的時(shí)間,并且在合并的期間,磁盤(pán)的 I/O 操作會(huì)比較頻繁,從而導(dǎo)致部分的CPU資源被占用,對(duì) MySQL 整體的性能是有影響的。
那你可能會(huì)問(wèn),難道只有被緩存的頁(yè)加載到了 Buffer Pool 才會(huì)觸發(fā)合并操作嗎?那要是它一直沒(méi)有被加載進(jìn)來(lái),Change Buffer 不就被撐爆了?很顯然,InnoDB 在設(shè)計(jì)的時(shí)候考慮到了這個(gè)點(diǎn)。除了對(duì)應(yīng)的頁(yè)加載,提交事務(wù)、服務(wù)停機(jī)、服務(wù)重啟都會(huì)觸發(fā)合并。
Adaptive Hash
自適應(yīng)哈希索引(Adaptive Hash Index)是配合 Buffer Pool 工作的一個(gè)功能。自適應(yīng)哈希索引使得MySQL的性能更加接近于內(nèi)存服務(wù)器。
如果要啟用自適應(yīng)哈希索引,可以通過(guò)更改配置innodb_adaptive_hash_index來(lái)開(kāi)啟。如果不想啟用,也可以在啟動(dòng)的時(shí)候,通過(guò)命令行參數(shù)--skip-innodb-adaptive-hash-index來(lái)關(guān)閉。
自適應(yīng)哈希索引是根據(jù)索引 Key 的前綴來(lái)構(gòu)建的,InnoDB 有自己的監(jiān)控索引的機(jī)制,當(dāng)其檢測(cè)到為當(dāng)前某個(gè)索引頁(yè)建立哈希索引能夠提升效率時(shí),就會(huì)創(chuàng)建對(duì)應(yīng)的哈希索引。如果某張表數(shù)據(jù)量很少,其數(shù)據(jù)全部都在 Buffer Pool 中,那么此時(shí)自適應(yīng)哈希索引就會(huì)變成我們所熟悉的指針這樣一個(gè)角色。
當(dāng)然,創(chuàng)建、維護(hù)自適應(yīng)哈希索引是會(huì)帶來(lái)一定的開(kāi)銷(xiāo)的,但是比起其帶來(lái)的性能上的提升,這點(diǎn)開(kāi)銷(xiāo)可以直接忽略不計(jì)。但是,是否要開(kāi)啟自適應(yīng)哈希索引還是需要看具體的業(yè)務(wù)情況的,例如當(dāng)我們的業(yè)務(wù)特征是有大量的并發(fā) Join 查詢(xún),此時(shí)訪問(wèn)自適應(yīng)哈希索引就會(huì)產(chǎn)生競(jìng)爭(zhēng)。
并且如果業(yè)務(wù)還使用了 LIKE 或者 % 等通配符,根本就不會(huì)用到哈希索引,那么此時(shí)自適應(yīng)哈希索引反而變成了系統(tǒng)的負(fù)擔(dān)。
所以,為了盡可能的減少并發(fā)情況下帶來(lái)的競(jìng)爭(zhēng),InnoDB 對(duì)自適應(yīng)哈希索引進(jìn)行了分區(qū),每個(gè)索引都被綁定到了一個(gè)特定的分區(qū),而每個(gè)分區(qū)都由單獨(dú)的鎖進(jìn)行保護(hù)。
其實(shí)通俗點(diǎn)理解,就是降低了鎖的粒度。分區(qū)的數(shù)量我們可以通過(guò)配置innodb_adaptive_hash_index_parts來(lái)改變,其可配置的區(qū)間范圍為[8, 512]。
過(guò)期策略
上面提到,Buffer Pool 也會(huì)有自己的過(guò)期策略,定時(shí)的將不需要的數(shù)據(jù)刷回磁盤(pán),為后續(xù)的請(qǐng)求騰出空間。那么,InnoDB 是怎么知道哪些數(shù)據(jù)是不需要的呢?
答案是 LRU 算法
LRU是**(L**east Recently Used)的簡(jiǎn)稱(chēng),表示最近最少使用,Redis 的內(nèi)存淘汰策略中也有用到 LRU。
但是 InnoDB 所采用的 LRU 算法和傳統(tǒng)的 LRU 算法還不太一樣,InnoDB 使用的是改良版的 LRU。那為啥要改良?這就需要了解原生 LRU 在 MySQL 有啥問(wèn)題了。
在實(shí)際的業(yè)務(wù)場(chǎng)景下,很有可能會(huì)出現(xiàn)全表掃描的情況,如果數(shù)據(jù)量較大,那么很有可能會(huì)將之前 Buffer Pool 中緩存的熱點(diǎn)數(shù)據(jù)全部換出。這樣一來(lái),熱點(diǎn)數(shù)據(jù)被再次訪問(wèn)時(shí),就需要執(zhí)行 I/O 操作,而這樣就會(huì)導(dǎo)致該段時(shí)間 MySQL 性能斷崖式下跌。而這種情況還有個(gè)專(zhuān)門(mén)的名詞,叫——緩沖池污染。
這也是為什么 InnoDB 要對(duì) LRU 算法做優(yōu)化。
優(yōu)化之后的鏈表被分成了兩個(gè)部分,分別是 New Sublist 和 Old Sublist,其分別占用了 Buffer Pool 的 3/4 和 1/4。
鏈表的前 3/4,也就是 New Sublist 存放的是訪問(wèn)較為頻繁的頁(yè)。而后 1/4 也就是 Old Sublist 則是反問(wèn)的不那么頻繁的頁(yè)。Old Sublist中的數(shù)據(jù),會(huì)在后續(xù) Buffer Pool 剩余空間不足、或者有新的頁(yè)加入時(shí)被移除掉。
了解了鏈表的整體構(gòu)造和組成之后,我們就以新頁(yè)被加入到鏈表為起點(diǎn),把整體流程走一遍。首先,一個(gè)新頁(yè)被放入到Buffer Pool之后,會(huì)被插入到鏈表中 New Sublist 和 Old Sublist 相交的位置,該位置叫MidPoint。
該鏈表存儲(chǔ)的數(shù)據(jù)來(lái)源有兩部分,分別是:
- MySQL 的預(yù)讀線程預(yù)先加載的數(shù)據(jù)
- 用戶(hù)的操作,例如 Query 查詢(xún)
默認(rèn)情況下,由用戶(hù)操作影響而進(jìn)入到 Buffer Pool 中的數(shù)據(jù),會(huì)被立即放到鏈表的最前端,也就是 New Sublist 的 Head 部分。但如果是 MySQL 啟動(dòng)時(shí)預(yù)加載的數(shù)據(jù),則會(huì)放入MidPoint中,如果這部分?jǐn)?shù)據(jù)再次被用戶(hù)訪問(wèn)過(guò)之后,才會(huì)放到鏈表的最前端。
這樣一來(lái),雖然這些頁(yè)數(shù)據(jù)在鏈表中了,但是由于沒(méi)有被訪問(wèn)過(guò),就會(huì)被移動(dòng)到后 1/4 的 Old Sublist中去,直到被清理掉。
Log Buffer
Log Buffer 用來(lái)存儲(chǔ)那些即將被刷入到磁盤(pán)文件中的日志,例如 Redo Log,該區(qū)域也是 InnoDB內(nèi)存的重要組成部分。Log Buffer 的默認(rèn)值為16M,如果我們需要進(jìn)行調(diào)整的話,可以通過(guò)配置參數(shù)innodb_log_buffer_size來(lái)進(jìn)行調(diào)整。
當(dāng) Log Buffer 如果較大,就可以存儲(chǔ)更多的 Redo Log,這樣一來(lái)在事務(wù)提交之前我們就不需要將 Redo Log 刷入磁盤(pán),只需要丟到 Log Buffer 中去即可。因此較大的 Log Buffer 就可以更好的支持較大的事務(wù)運(yùn)行;同理,如果有事務(wù)會(huì)大量的更新、插入或者刪除行,那么適當(dāng)?shù)脑龃? Log Buffer 的大小,也可以有效的減少部分磁盤(pán) I/O 操作。
至于 Log Buffer 中的數(shù)據(jù)刷入到磁盤(pán)的頻率,則可以通過(guò)參數(shù)innodb_flush_log_at_trx_commit來(lái)決定。