MySQL 不完全入門指南

由於 MySQL 的整個體系太過於龐大,文章的篇幅有限,不能夠完全的覆蓋所有的方面。所以我會盡可能的從更加貼進我們日常使用的方式來進行解釋。

小白眼中的 MySQL

首先,對於我們來說,MySQL 是個啥?我們從一個最簡單的例子來回顧一下。

這可能就是最開始大家認知中的 MySQL。那 MySQL 中是怎麼處理這個查詢語句的呢?換句話說,它是如何感知到這串字符串是一個查詢語句的?它是如何感知到該去哪張表中取數據?它是如何感知到該如何取數據?

到目前爲止,都不知道。接下來我們一步一補來進行解析。

連接池

首先,要去 MySQL 執行命令,肯定是需要連接上 MySQL 服務器的,就像我們通過「用戶名」和「密碼」登陸網站一樣。所以,我們首先要認識的就是連接池

這種池化技術的作用很明顯,複用連接,避免頻繁的銷燬、創建線程所帶來的開銷。除此之外,在這一層還可以根據你的賬號密碼對用戶的合法性、用戶的權限進行校驗。

每一個連接都對應一個線程,「服務器」 和 「MySQL」 都一樣,服務器的一個線程從服務器的連接池中取出一個連接,發起查詢語句。MySQL 服務器的線程從連接池中取出一個線程,繼續後續的流程。

那麼後面的流程是啥呢?當然是分析 SQL 語句了。

分析器

很明顯,MySQL 肯定得知道這個 SQL 是不是個合法的 SQL 語句,以及 SQL 語句到底要幹啥?

就好像有個哥們瘋狂的敲你家門,門打開了,下一步是幹嘛?肯定得問他是誰?來幹嘛?

所以,下一步就是要將 SQL 進行解析。解析完成之後,我們就知道當前的 SQL 是否符合語法,它到底要幹嘛,是要查詢數據?還是要更新數據?還是要刪除數據?

很簡單,我們肉眼能能明顯看出來一條 SQL 語句是要幹嘛。但電腦不是人腦,我們得讓電腦也能看懂這條 SQL 語句,才能幫我們去做後面的事。

知道了這個 SQL 語言要幹嘛之後,是不是就可以開始執行操作了呢?

並不是

優化器

MySQL 除了要知道這條 SQL 要幹嘛,在執行之前,還得決定怎麼幹,怎麼幹是最優解。

還是剛剛那個例子,隔壁的哥們敲開了你家的門,說哥們兒,我家裏這停水了,想找你施(白)舍(漂) 24 瓶礦泉水。我們暫且不去討論,他爲什麼需要 24 瓶礦泉水。

我們要討論的是,我們要怎麼把這 24 瓶礦泉水拿給他。因爲你剛剛想起來,礦泉水在之前被你一頓操作全扔櫃子了。

你是要每次拿個 4 瓶,跑 6 趟呢?還是找個箱子,把 24 瓶裝滿再搬出去給他。

這差不多就是優化器要做的事情,優化器會分析你的 SQL,找出最優解。

再舉個正經的例子,假設 SELECTnameFROMstudentwhereid= 1 語句執行時,數據庫裏有 1W 條數據,此時有兩種方案:

  1. 查出所有列的 name 值,然後再遍歷對比,找到 id=1 的 name 值
  2. 直接找到 id=1 的數據,然後再取 name 的值

用屁股想想都知道應該選方案2.

找到了怎麼做之後,接下來就需要落實到行動上了。所以,接下來執行器閃亮登場。

執行器

執行器會掉用底層存儲引擎的接口,來真正的執行 SQL 語句,在這裏的例子就是查詢操作。

至此,MySQL 這個黑盒子已經被我們一步一步的揭開了面紗。但是在揭開最後一片面紗的時候,我們又發現了新的黑盒子。那就是存儲引擎

我們到目前爲止,就只知道它的名字,至於其如何存儲數據,如何查詢數據,一概不知。

存儲引擎

MySQL 的存儲引擎有很多的種類,分別適用於不同的場景。大家可以這麼理解,存儲引擎就是一個執行數據操作的接口(Interface),而底層的具體實現有很多類。

InnoDB、MyISAM、Memory、CSV、Archive、Blackhole、Merge、Federated、Example

用的最廣泛的,就是 InnoDB 了。打從 MySQL 5.5 之後,InnoDB 就是 MySQL 默認的存儲引擎了。

所以,存儲引擎其實並不是什麼高大上的東西,跟什麼大學拿去交作業的圖書館管理系統區別,就差了點而已。

或者,我再舉個例子。我們往我們電腦上的文件中寫入數據,此時由於 OS 的優化,數據並不會直接寫入磁盤,因爲 I/O 操作相當的昂貴。數據會先進入到 OS 的 Cache 中,由 OS 之後刷入磁盤。而 MySQL 在整個的邏輯結構上,跟計算機寫文件差不多。

從上面的例子看出,存儲引擎可以分爲兩部分:

  • 內存

  • 磁盤

所以,從宏觀上來說,MySQL 就是把數據在緩存在內存中,鼓搗鼓搗,然後在某些時候刷入磁盤中去,就這麼回事。

接下來,就讓我們深入存儲引擎 InnoDB 的底層原理中相當重要的一部分——內存架構

簡單來說,InnoDB 的內存由以下兩部分組成:

  • Buffer Pool

  • Log Buffer

從上面畫的圖就能夠看出,Buffer Pool 是 InnoDB 中非常重要的一部分,MySQL 之所以這麼快其中一個重要的原因就是其數據都存在內存中,而這個內存就是 Buffer Pool。

Buffer Pool

一般來說,宿主機的 80% 的內存都會分配給 Buffer Pool。這個很好理解,內存越大,就能夠存下更多的數據。這樣一來更多的數據將可以直接在內存中讀取,可以大大的提升操作效率。

那 Buffer Pool 中到底是如何存儲數據的呢?如果其底層的數據存儲不進行特殊的設計、優化,那麼 InnoDB 在取數據的時候除了整個遍歷之外,沒有其他的捷徑。而如果那樣做,MySQL 也不會獲得今天這樣的地位。

如果我們能想象一下,InnoDB 會如何組織內存的數據。想象一下,圖書館的書是直接一本一本的攤在地上好找,還是按照類目、名稱進行分類、放到對應的書架上、再進行編號好找?結論自然不言而喻。Buffer Pool 也採用了同樣的數據整合措施。

InnoDB 將 Buffer Pool 分成了一張一張的頁(Pages),同時還有個 Change Buffer(後面會詳細講,這裏先知道就行)。分成一頁一頁的數據就能夠提升查詢效率嗎?那這個頁裏面到底是個啥呢?

可以從上圖看到,頁和頁之間,實際上是有關聯的,他們通過雙向鏈表進行連接,可以方便的從某一頁跳到下一頁。

那數據在頁中具體是如何存儲的呢?

User Records

當然,光跳來跳去的並不能說明任何問題,我們還是揭開頁(Pages)這個黑盒的面紗吧。

!

可以看到,主要就分爲

  • 上一頁指針

  • 下一頁指針

  • User Records

  • 其餘字段

爲了方便理解,其餘字段我後續會補充

上一頁指針、下一頁指針就不多贅述,就是一個指針。重點我們需要了解 User Records。

User Records 就是一行一行的數據**(Rows)最終存儲的地方,其中,行與行之間形成了單向鏈表**。

看了這個單向鏈表不知道你有沒有一個疑問。

我們知道,在聚簇索引中,Key 實際上會按照 Primary Key 的順序來進行排列。那在 User Records 中也會這樣嗎?我們插入一條新的數據到 User Records 中時,是否也會按照 Primary Key 的順序來對已有的數據重排序?

如果每次插入數據都需要對 User Records 中的數據進行重排序,那麼 MySQL 的江湖地位將再次不保。

雖然在圖中看起來是按照「主鍵」的順序存儲的,但實際上是按照數據的插入順序來存儲的,先到的數據會在前面,後到的數據會在後面,只是每個 User Records 數據的指針指向的不是物理上的下一個,而是邏輯上的下一個

用圖來表示,大概如下:

可以理解爲數組和鏈表的區別。

看到這,那麼問題來了,說好的不遍歷呢?這不是打臉嗎?因爲從上圖可以看出,我要找查找某個數據是否存在於當前的頁(Pages)中,只能從頭開始遍歷這個單向鏈表。

就這?還敢號稱高性能?當然,InnoDB 肯定不是這麼搞的。這下就需要從「其餘字段」中取出一部分字段了來解釋了。

Infimum 和 Supremum

分別代表當前頁(Pages)中的最大最小的記錄。

可以看到,有了 InfimumSupremum,我們不需要再去遍歷 User Records 就能夠知道,要找的數據是否在當前的頁中,大大的提升了效率。

但其實還是有問題,比如我需要查詢的數據不在當前頁中還好,那如果在呢?那是不是還是逃不了 O(N) 的鏈表遍歷呢?算不算治標不治本?

這個時候,我們又需要從「其餘字段」中抽一個概念出來了。

Page Directory

顧名思義,這玩意兒是個「目錄」,可以看下圖。

!

可以看到,每隔一段記錄就會在 Page Directory 中有個記錄,這個記錄是一個指向 User Records 中記錄的一個指針。

不知道這個設計有沒有讓你想起跳錶(Skip List)。那這個 Page Directory 中的目錄拿來幹嘛呢?

有了 Page Directory,就可以對一頁中的數據進行類似於跳錶的中的查詢。在 Page Directory 中找到對應的「位置」之後,再根據指針跳到對應的 User Records 上的單鏈表,進行查詢。如此一來就避免了遍歷全部的數據。

上面提到的「位置」,其實有個專業的名詞叫「槽位(Slots)」。每一個槽位的數據都是一個指向了 User Records 某條記錄的指針。

當我們新增每條數據的時候,就會同步的對 Page Directory 中的槽位進行維護。InnoDB 規定每隔 6 條記錄就會創建一個 Slot。

瞭解到這裏之後,關於如何高效地在 MySQL 查詢數據就已經瞭解的差不多了。

想了解「其餘字段」還有哪些、以及「頁」的完整面貌的,可以去看看我之前寫的頁的文章 MySQL 頁完全指南——淺入深出頁的原理,再次就不再贅述。

索引

瞭解完頁之後,索引是什麼就一目瞭然了。InnoDB 底層的存儲使用的數據結構爲 B+樹,B樹的變種。MySQL 中有兩種索引,分別是聚簇索引和非聚簇索引,聽着很高大上。

其實瞭解完「頁」的底層原理,要區分它們就變成的很簡單了。

  • 聚簇索引的葉子結點上,存儲的是「頁」

  • **非聚簇索引(二級索引)**的葉子結點上,存儲的是「主鍵ID」。很多時候,我們都需要通過非聚簇索引拿到主鍵,再根據這個主鍵去「聚簇索引」中拿完整的數據,這個過程還有一個很有意思的名字叫「回表」。

至於爲什麼底層數據結構要用 B+樹 和 B樹,大概是因爲以下三點:

  • B+樹能夠減少 I/O 的次數

  • 查詢效率更加的穩定

  • 能夠更好的支持範圍查詢

詳細的原因可以參考之前寫的 淺入淺出 MySQL 索引

更新數據

爲什麼下一步就是要看如何更新數據呢?因爲上述的「頁」的原理主要都是基於「查詢」的前提在講,看完了之後對查詢的過程應該瞭然於胸了。接下來我們就來看看更新的時候會發生什麼。

首先,如果我們插入了某條 id=100 的數據,然後再去更新的話,這條數據是一定的在 Buffer Pool 的。這句話看似是廢話(我都寫到數據庫了那肯定存在啊)

那我換個說法,更新的時候,id=100 這條數據可能不在 Buffer Pool 中。爲什麼之前寫入了 Buffer Pool,之後再來更新 Buffer Pool 中又沒有呢?

答案是內存是有限的,我們不可能無限的向 Buffer Pool 中插入數據。熟悉 Redis 的知道,Redis 在運行時會有「過期策略」,有以下三種:

  1. 定時過期

  2. 惰性過期

  3. 定期過期

而 Buffer Pool 同樣也是基於內存,同樣也需要一個「過期策略」來清理掉一些不常被訪問的數據,來爲新的數據、熱點數據騰出空間。

當然,這裏的清理掉,並不是刪除,而是將它們刷入磁盤

更新數據時,如果發現對應的數據不存在,就會將那個數據所在的頁加載到 Buffer Pool 中來。注意,這裏並不是只加載 id=100 這一行,而是其所在的一整「頁」數據。

加載到 Buffer Pool 中之後,再對 Buffer Pool 中的數據進行更新。當然,這個情況對我們開發人員來說,是針對聚簇索引的。

還有另一種情況是針對「 非聚簇索引」 的。

Change Buffer

很簡單,當我們更新了某些字段之後,假設這些字段是組成非聚簇索引的字段,就會涉及到非聚簇索引的更新,但不巧的是該非聚簇索引所在的頁不在 Buffer Pool 中。按照之前的說法,需要將對應的頁(Pages)加載到 Buffer Pool 中來。

但是這裏有一個很大的問題,這個二級索引可能之後**根本不會被用到,**那這樣一來,剛剛昂貴的 I/O 操作就被浪費掉了。積少成多,如果每次涉及到更新二級索引發現在 Buffer Pool 中不存在,都去做 I/O 操作,那也是一個相當大的開銷。

所以,InnoDB 才設計了 Change Buffer。Change Buffer 就是專門用來存儲當「非聚簇索引」所在的頁不在 Buffer Pool 時的更改的。

換句話說,當對應的非聚簇索引被修改並且對應的頁(Pages)不在 Buffer Pool 中時,會將其改動暫存在 Change Buffer,等到其對應的頁被其他的請求加載進 Buffer Pool 時,就會將 Change Buffer 中暫存的數據 和 Buffer Pool 中的數據進行合併

當然,Change Buffer 這個設計也不是沒有缺點。當 Change Buffer 中有很多的數據時,全部合併到Buffer Pool可能會花上幾個小時的時間,並且在合併的期間,磁盤的 I/O 操作會比較頻繁,從而導致部分的CPU資源被佔用,對 MySQL 整體的性能是有影響的。

那你可能會問,難道只有被緩存的頁加載到了 Buffer Pool 纔會觸發合併操作嗎?那要是它一直沒有被加載進來,Change Buffer 不就被撐爆了?很顯然,InnoDB 在設計的時候考慮到了這個點。除了對應的頁加載,提交事務、服務停機、服務重啓都會觸發合併。

Adaptive Hash

自適應哈希索引(Adaptive Hash Index)是配合 Buffer Pool 工作的一個功能。自適應哈希索引使得MySQL的性能更加接近於內存服務器。

如果要啓用自適應哈希索引,可以通過更改配置innodb_adaptive_hash_index來開啓。如果不想啓用,也可以在啓動的時候,通過命令行參數--skip-innodb-adaptive-hash-index來關閉。

自適應哈希索引是根據索引 Key 的前綴來構建的,InnoDB 有自己的監控索引的機制,當其檢測到爲當前某個索引頁建立哈希索引能夠提升效率時,就會創建對應的哈希索引。如果某張表數據量很少,其數據全部都在 Buffer Pool 中,那麼此時自適應哈希索引就會變成我們所熟悉的指針這樣一個角色。

當然,創建、維護自適應哈希索引是會帶來一定的開銷的,但是比起其帶來的性能上的提升,這點開銷可以直接忽略不計。但是,是否要開啓自適應哈希索引還是需要看具體的業務情況的,例如當我們的業務特徵是有大量的併發 Join 查詢,此時訪問自適應哈希索引就會產生競爭

並且如果業務還使用了 LIKE 或者 % 等通配符,根本就不會用到哈希索引,那麼此時自適應哈希索引反而變成了系統的負擔。

所以,爲了儘可能的減少併發情況下帶來的競爭,InnoDB 對自適應哈希索引進行了分區,每個索引都被綁定到了一個特定的分區,而每個分區都由單獨的鎖進行保護。

其實通俗點理解,就是降低了鎖的粒度。分區的數量我們可以通過配置innodb_adaptive_hash_index_parts來改變,其可配置的區間範圍爲[8, 512]。

過期策略

上面提到,Buffer Pool 也會有自己的過期策略,定時的將不需要的數據刷回磁盤,爲後續的請求騰出空間。那麼,InnoDB 是怎麼知道哪些數據是不需要的呢?

答案是 LRU 算法

LRU是**(L**east Recently Used)的簡稱,表示最近最少使用,Redis 的內存淘汰策略中也有用到 LRU。

但是 InnoDB 所採用的 LRU 算法和傳統的 LRU 算法還不太一樣,InnoDB 使用的是改良版的 LRU。那爲啥要改良?這就需要了解原生 LRU 在 MySQL 有啥問題了。

在實際的業務場景下,很有可能會出現全表掃描的情況,如果數據量較大,那麼很有可能會將之前 Buffer Pool 中緩存的熱點數據全部換出。這樣一來,熱點數據被再次訪問時,就需要執行 I/O 操作,而這樣就會導致該段時間 MySQL 性能斷崖式下跌。而這種情況還有個專門的名詞,叫——緩衝池污染

這也是爲什麼 InnoDB 要對 LRU 算法做優化。

優化之後的鏈表被分成了兩個部分,分別是 New SublistOld Sublist,其分別佔用了 Buffer Pool 的 3/4 和 1/4。

鏈表的前 3/4,也就是 New Sublist 存放的是訪問較爲頻繁的頁。而後 1/4 也就是 Old Sublist 則是反問的不那麼頻繁的頁。Old Sublist中的數據,會在後續 Buffer Pool 剩餘空間不足、或者有新的頁加入時被移除掉。

瞭解了鏈表的整體構造和組成之後,我們就以新頁被加入到鏈表爲起點,把整體流程走一遍。首先,一個新頁被放入到Buffer Pool之後,會被插入到鏈表中 New Sublist 和 Old Sublist 相交的位置,該位置叫MidPoint

該鏈表存儲的數據來源有兩部分,分別是:

  • MySQL 的預讀線程預先加載的數據

  • 用戶的操作,例如 Query 查詢

默認情況下,由用戶操作影響而進入到 Buffer Pool 中的數據,會被立即放到鏈表的最前端,也就是 New Sublist 的 Head 部分。但如果是 MySQL 啓動時預加載的數據,則會放入MidPoint中,如果這部分數據再次被用戶訪問過之後,纔會放到鏈表的最前端。

這樣一來,雖然這些頁數據在鏈表中了,但是由於沒有被訪問過,就會被移動到後 1/4 的 Old Sublist中去,直到被清理掉。

Log Buffer

Log Buffer 用來存儲那些即將被刷入到磁盤文件中的日誌,例如 Redo Log,該區域也是 InnoDB內存的重要組成部分。Log Buffer 的默認值爲16M,如果我們需要進行調整的話,可以通過配置參數innodb_log_buffer_size來進行調整。

當 Log Buffer 如果較大,就可以存儲更多的 Redo Log,這樣一來在事務提交之前我們就不需要將 Redo Log 刷入磁盤,只需要丟到 Log Buffer 中去即可。因此較大的 Log Buffer 就可以更好的支持較大的事務運行;同理,如果有事務會大量的更新、插入或者刪除行,那麼適當的增大 Log Buffer 的大小,也可以有效的減少部分磁盤 I/O 操作。

至於 Log Buffer 中的數據刷入到磁盤的頻率,則可以通過參數innodb_flush_log_at_trx_commit來決定。

本篇文章已放到我的 Github github.com/sh-blog 中,歡迎 Star。微信搜索關注【SH的全棧筆記】,回覆【隊列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的源碼解析,持續更新中。

如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章