mysql高頻面試題,一文詳解Innodb存儲引擎原理

頓頓有菜ashin 發佈 2024-05-10T10:19:51.318048+00:00

大綱本文主要講解InnoDB存儲引擎的執⾏原理,在此引入幾個問題:數據頁和緩存頁是什麼?如何知道哪些緩存頁是空閒的,哪些緩存頁是可被清除的?mysql預讀機制了解過嗎,什麼情況下會觸發它?mysql是為了應對什麼樣的場景才設計預讀機制?

大綱

本文主要講解innodb存儲引擎的執⾏原理,在此引入幾個問題:

  1. 數據頁和緩存頁是什麼?如何知道哪些緩存頁是空閒的,哪些緩存頁是可被清除的?
  2. MySQL預讀機制了解過嗎,什麼情況下會觸發它?mysql是為了應對什麼樣的場景才設計預讀機制?
  3. 類⽐redis在內存中也存在冷熱數據共存的場景,如何考慮利⽤lru鍊表解決預讀機制的思想、來對redis 緩存的設計進⾏優化?
  4. 下面的圖描述了整個執行過程:

磁碟數據如何加載到mysql中?

⼀般我們要更新⼀條數據,數據⼀開始肯定是存放在磁碟中的,用到時才會被加載到 mysql,存放的數據在邏輯概念上我們稱為表,物理層⾯上在磁碟中是按數據頁形式存放的, 那麼加載到mysql中的就稱為緩存頁。 每個緩存頁都有對應的⼀份描述信息,存放了緩存⻚的⼀些元數據相關的⼀些信息,通過 描述信息可以快速定位到緩存頁,最開始描述信息指向的緩存⻚當然都是空閒沒有數據的,從 磁碟加載數據頁信息。

mysql在磁碟以一頁16K的數據按頁存儲,所以在初始化buffer pool的時候,同時也將內存按頁劃分,作為緩存頁。

Buffer Pool 是在 MySQL 啟動的時候,向作業系統申請的一片連續的內存空間,默認配置下 Buffer Pool 只有 128MB

可以通過調整 innodb_buffer_pool_size 參數來設置 Buffer Pool 的大小,一般建議設置成可用物理內存的 60%~80%

當有了Buffer Pool後,它的作用:

  • 當讀取數據時,如果數據存在於 Buffer Pool 中,客戶端就會直接讀取 Buffer Pool 中的數據,否則再去磁碟中讀取。
  • 當修改數據時,首先是修改 Buffer Pool 中數據所在的頁,然後將其頁設置為髒頁,最後由後台線程將髒頁寫入到磁碟

如何快速找到空閒緩存頁

既然現在我們已經知道磁碟中的數據頁是加載到buffer pool緩衝池中的,那麼我們怎麼樣才能知道哪些緩存頁是空閒的?哪些緩存頁是沒有被加載過數據頁信息的呢?

為了能夠快速找到空閒的緩存頁,可以使用鍊表結構,將空閒緩存頁的「描述信息」作為鍊表的節點,這個鍊表稱為 Free 鍊表(空閒鍊表)。

有了 Free 鍊表後,每當需要從磁碟中加載一個頁到 Buffer Pool 中時,就從 Free鍊表中取一個空閒的緩存頁,並且把該緩存頁對應的控制塊的信息填上,然後把該緩存頁對應的控制塊從 Free 鍊表中移除。

此時數據頁被加載到緩存頁了,緩存頁中已經有數據了,相關的變動信息肯定也要回寫到 描述信息中,並且現在因為緩存頁已經有數據,就不能再待在free鍊表中了,就需要將該緩存頁對應的描述信息節點從free鍊表給摘掉,轉移到了LRU鍊表中,如下圖所示

lru鍊表實現的目的就是為讓哪些被訪問的緩存頁能夠儘量排到靠前位置,那麼此時如果此 時內存不夠需要淘汰掉⼀些緩存頁時,此時就可以到lru鍊表尾部,將哪些最近最少被訪問的 尾部節點給刷盤釋放緩存頁騰出內存來。

簡單的 LRU 算法的實現思路是這樣的:

  • 當訪問的頁在 Buffer Pool 里,就直接把該頁對應的 LRU 鍊表節點移動到鍊表的頭部。
  • 當訪問的頁不在 Buffer Pool 里,除了要把頁放入到 LRU 鍊表的頭部,還要淘汰 LRU 鍊表末尾的節點。

簡單的 LRU 算法並沒有被 MySQL 使用,因為簡單的 LRU 算法無法避免下面這兩個問題:

  • 預讀失效;
  • Buffer Pool 污染;


預讀失效

先來說說 MySQL 的預讀機制。程序是有空間局部性的,靠近當前被訪問數據的數據,在未來很大概率會被訪問到。

所以,MySQL 在加載數據頁時,會提前把它相鄰的數據頁一併加載進來,目的是為了減少磁碟 IO。

但是可能這些被提前加載進來的數據頁,並沒有被訪問,相當於這個預讀是白做了,這個就是預讀失效

如果使用簡單的 LRU 算法,就會把預讀頁放到 LRU 鍊表頭部,而當 Buffer Pool空間不夠的時候,還需要把末尾的頁淘汰掉。

如果這些預讀頁如果一直不會被訪問到,就會出現一個很奇怪的問題,不會被訪問的預讀頁卻占用了 LRU 鍊表前排的位置,而末尾淘汰的頁,可能是頻繁訪問的頁,這樣就大大降低了緩存命中率。

要避免預讀失效帶來影響,最好就是讓預讀的頁停留在 Buffer Pool 里的時間要儘可能的短,讓真正被訪問的頁才移動到 LRU 鍊表的頭部,從而保證真正被讀取的熱數據留在 Buffer Pool 里的時間儘可能長

那到底怎麼才能避免呢

優化後的lru鍊表主要引⼊了冷熱數據分離的思想解決了mysql預讀機制帶來的問題。把 lru鍊表分為熱數據區和冷數據區,熱數據區主要存放那些訪問頻率⾼的緩存⻚,冷數據區存放訪問頻率較低的緩存頁;從磁碟加載數據到lru鍊表時,⾸先會將加載到的緩存頁直接先放 到冷數據鏈的表頭,如果1000ms(默認,可配置)後冷數據的緩存頁又被訪問了,此時就認 為這些1000ms之後被訪問的緩存頁,在不久的未來可能還會被訪問,可以認為它們是熱數據 了,就會把這些緩存頁從冷數據區的鍊表給移動到熱數據區鍊表的表頭,通過該步驟可以將熱 數據從冷數據堆中給巧妙的分離出來。

髒頁什麼時候會被刷入磁碟?

引入了 Buffer Pool 後,當修改數據時,首先是修改 Buffer Pool 中數據所在的頁,然後將其頁設置為髒頁,但是磁碟中還是原數據。

因此,髒頁需要被刷入磁碟,保證緩存和磁碟數據一致,但是若每次修改數據都刷入磁碟,則性能會很差,因此一般都會在一定時機進行批量刷盤。

InnoDB 的更新操作採用的是 Write Ahead Log 策略,即先寫日誌,再寫入磁碟,通過 redo log 日誌讓 MySQL 擁有了崩潰恢復能力

為此,innodb設計了flush鍊表,在緩衝池中被更新過數據的緩存頁,這些緩存⻚的描述信息都會被添加到flush鍊表中。

下面幾種情況會觸發髒頁的刷新:

  • 當 redo log 日誌滿了的情況下,會主動觸發髒頁刷新到磁碟;
  • Buffer Pool 空間不足時,需要將一部分數據頁淘汰掉,如果淘汰的是髒頁,需要先將髒頁同步到磁碟;
  • MySQL 認為空閒時,後台線程會定期將適量的髒頁刷入到磁碟;
  • MySQL 正常關閉之前,會把所有的髒頁刷入到磁碟;
關鍵字: