乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?

專注於Java領域優質技術,歡迎關注

以下文章來源於柳樹的絮叨叨 ,作者靠髮型吃飯的柳樹

上一講:MySQL 是如何實現 ACID 中的 D 的? 我用了一個問題,給大家介紹了 MySQL 中的兩個成員 binlog 和 redo log。然而,這只是 MySQL 家族裡的兩個小嘍囉,Mysql 可以做到高性能高可靠,靠的絕對不只有他們倆。

MySQL 裡還有什麼其他成員呢?

對於 MySQL,要記住、或者要放在你隨時可以找到的地方的兩張圖,一張是 MySQL 架構圖,另一張則是 InnoDB 架構圖:

乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


遇到問題,或者學習到新知識點時,就往裡套,想一想,這是對應這兩張圖的哪個模塊、是屬於具體哪個成員的能力。

這其中,第一張圖的最底下的存儲引擎層(Storage Engines),它決定了 MySQL 會怎樣存儲數據,怎樣讀取和寫入數據,也在很大程度上決定了 MySQL 的讀寫性能和數據可靠性。

對於這麼重要的一層能力,MySQL 提供了極強的擴展性,你可以定義自己要使用什麼樣的存儲引擎:InnoDB、MyISAM、MEMORY、CSV,甚至可以自己開發一個存儲引擎然後使用它。

我一直覺得 MySQL 的設計,是教科書式的,高內聚松耦合,邊界明確,職責清晰。學習 MySQL,學的不只是如何更好的使用 MySQL,更是學習如何更好的進行系統設計。

通常我們說 Mysql 高性能高可靠,都是指基於 InnoDB 存儲引擎的 Mysql,所以,這一講,先讓我們來看看,除了 redo log,InnoDB 裡還有哪些成員,他們都有什麼能力,承擔了什麼樣的角色,他們之間又是怎麼配合的?

InnoDB 內存架構

從上面第二張圖可以看到,InnoDB 主要分為兩大塊:

  • InnoDB In-Memory Structures
  • InnoDB On-Disk Structures


內存和磁盤,讓我們先從內存開始。

1、Buffer Pool

The buffer pool is an area in main memory where InnoDB caches table and index data as it is accessed.

正如之前提到的,MySQL 不會直接去修改磁盤的數據,因為這樣做太慢了,MySQL 會先改內存,然後記錄 redo log,等有空了再刷磁盤,如果內存裡沒有數據,就去磁盤 load。

而這些數據存放的地方,就是 Buffer Pool。

我們平時開發時,會用 redis 來做緩存,緩解數據庫壓力,其實 MySQL 自己也做了一層類似緩存的東西。

MySQL 是以「頁」(page)為單位從磁盤讀取數據的,Buffer Pool 裡的數據也是如此,實際上,Buffer Pool 是a linked list of pages,一個以頁為元素的鏈表。

為什麼是鏈表?因為和緩存一樣,它也需要一套淘汰算法來管理數據。

Buffer Pool 採用基於 LRU(least recently used) 的算法來管理內存:

乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


關於 Buffer Pool 的更多知識,諸如如何配置大小、如何監控等等:Buffer Pool

2、Change Buffer

上面提到過,如果內存裡沒有對應「頁」的數據,MySQL 就會去把數據從磁盤裡 load 出來,如果每次需要的「頁」都不同,或者不是相鄰的「頁」,那麼每次 MySQL 都要去 load,這樣就很慢了。

於是如果 MySQL 發現你要修改的頁,不在內存裡,就把你要對頁的修改,先記到一個叫 Change Buffer 的地方,同時記錄 redo log,然後再慢慢把數據 load 到內存,load 過來後,再把 Change Buffer 裡記錄的修改,應用到內存(Buffer Pool)中,這個動作叫做

merge;而把內存數據刷到磁盤的動作,叫 purge

  • merge:Change Buffer -> Buffer Pool
  • purge:Buffer Pool -> Disk


乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


The change buffer is a special data structure that caches changes to secondary index pages when those pages are not in the buffer pool. The buffered changes, which may result from INSERT, UPDATE, or DELETE operations (DML), are merged later when the pages are loaded into the buffer pool by other read operations.

上面是 MySQL 官網對 Change Buffer 的定義,仔細看的話,你會發現裡面提到:Change Buffer 只在操作「二級索引」(secondary index)時才使用,原因是「聚簇索引」(clustered indexes)必須是「唯一」的,也就意味著每次插入、更新,都需要檢查是否已經有相同的字段存在,也就沒有必要使用 Change Buffer 了;另外,「聚簇索引」操作的隨機性比較小,通常在相鄰的「頁」進行操作,比如使用了自增主鍵的「聚簇索引」,那麼 insert 時就是遞增、有序的,不像「二級索引」,訪問非常隨機。

如果想深入理解 Change Buffer 的原理,除了 MySQL 官網的介紹:Change Buffer,還可以閱讀下《MySQL技術內幕》的「2.6.1 - 插入緩衝」章節,裡面會從 Change Buffer 的前身 —— Insert Buffer 開始講起,很透徹。

3、Adaptive Hash Index

MySQL 索引,不管是在磁盤裡,還是被 load 到內存後,都是 B+ 樹,B+ 樹的查找次數取決於樹的深度。你看,數據都已經放到內存了,還不能“一下子”就找到它,還要“幾下子”,這空間犧牲的是不是不太值得?

尤其是那些頻繁被訪問的數據,每次過來都要走 B+ 樹來查詢,這時就會想到,我用一個指針把數據的位置記錄下來不就好了?

這就是「自適應哈希索引」(Adaptive Hash Index)。自適應,顧名思義,MySQL 會自動評估使用自適應索引是否值得,如果觀察到建立哈希索引可以提升速度,則建立。

4、Log Buffer

The log buffer is the memory area that holds data to be written to the log files on disk.

從上面架構圖可以看到,Log Buffer 裡的 redo log,會被刷到磁盤裡:

乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


Operating System Cache

在內存和磁盤之間,你看到 MySQL 畫了一層叫做 Operating System Cache 的東西,其實這個不屬於 InnoDB 的能力,而是操作系統為了提升性能,在磁盤前面加的一層高速緩存,這裡不展開細講,感興趣的同學可以參考下維基百科:Page Cache

InnoDB 磁盤架構

磁盤裡有什麼呢?除了表結構定義和索引,還有一些為了高性能和高可靠而設計的角色,比如 redo log、undo log、Change Buffer,以及 Doublewrite Buffer 等等.

有同學會問,那表的數據呢?其實只要理解了 InnoDB 裡的所有表數據,都以索引(聚簇索引+二級索引)的形式存儲起來,就知道索引已經包含了表數據。

1、表空間(Tablespaces)

從架構圖可以看到,Tablespaces 分為五種:

  • The System Tablespace
  • File-Per-Table Tablespaces
  • General Tablespace
  • Undo Tablespaces
  • Temporary Tablespaces

其中,我們平時創建的表的數據,可以存放到 The System Tablespace 、File-Per-Table Tablespaces、General Tablespace 三者中的任意一個地方,具體取決於你的配置和創建表時的 sql 語句。

這裡同樣不展開,如何選擇不同的表空間存儲數據?不同表空間各自的優勢劣勢等等,傳送門:Tablespaces

2、Doublewrite Buffer

如果說 Change Buffer 是提升性能,那麼 Doublewrite Buffer 就是保證數據頁的可靠性。

怎麼理解呢?

前面提到過,MySQL 以「頁」為讀取和寫入單位,一個「頁」裡面有多行數據,寫入數據時,MySQL 會先寫內存中的頁,然後再刷新到磁盤中的頁。

這時問題來了,假設在某一次從內存刷新到磁盤的過程中,一個「頁」刷了一半,突然操作系統或者 MySQL 進程奔潰了,這時候,內存裡的頁數據被清除了,而磁盤裡的頁數據,刷了一半,處於一箇中間狀態,不尷不尬,可以說是一個「不完整」,甚至是「壞掉的」的頁。

有同學說,不是有 Redo Log 麼?其實這個時候 Redo Log 也已經無力迴天,Redo Log 是要在磁盤中的頁數據是正常的、沒有損壞的情況下,才能把磁盤裡頁數據 load 到內存,然後應用 Redo Log。而如果磁盤中的頁數據已經損壞,是無法應用 Redo Log 的。

所以,MySQL 在刷數據到磁盤之前,要先把數據寫到另外一個地方,也就是 Doublewrite Buffer,寫完後,再開始寫磁盤。Doublewrite Buffer 可以理解為是一個備份(recovery),萬一真的發生 crash,就可以利用 Doublewrite Buffer 來修復磁盤裡的數據。

留個問題,有了 Doublewrite Buffer 後,不就意味著 MySQL 要寫兩次磁盤?性能豈不是很差?

未完待續

讓我們再來回顧一下這張圖:

乾貨!MySQL 的 InnoDB 存儲引擎是怎麼設計的?


這篇文章,順著這張圖,給大家介紹了 InnoDB 裡的每一個成員、成員各自扮演的角色、提供的能力。

當然,這張圖裡能表達的信息是有限的,我習慣稱這種圖為「架構圖」,或者「模塊圖」。

用 DDD 的話來講,這張圖可以告訴你,MySQL 裡有哪些「域」(子域、核心域、通用域、支撐域),配合文字介紹,可以知道這些「域」之間都有什麼樣的能力、行為,知道「域」之間一些簡單的交互。

然而,這張圖並沒有告訴你具體某個業務中,這些成員之間要如何配合,來提供一個服務,或者說,如果你的技術方案裡只有這張圖,那你進入開發階段後,最多最多,只能新建幾個微服務應用,新建幾個類和對象,而寫不出這些個微服務、class 之間如何協作起來提供一個服務的代碼。

所以,下一篇文章,將基於我們這篇文章以及上一篇文章的內容,畫出一張足以描述具體業務流程的圖。

什麼樣的圖有這種描述力呢?

自然是 swim-lanes,也就是我們常說的「泳道圖」。

在那之後,我們將深入到每一個細分領域,以及具體到一些實際問題中,來把 MySQL 徹底學透。


分享到:


相關文章: