一文搞懂 MySQL InnoDB架構 Buffer Pool、Change Buffer、自適應哈希索引、Log Buffer
InnoDB 架構誕生
2003 年 12 月 24 日,平安夜,林淵從維修臺猛然驚醒,耳邊是 DBA 的怒吼:"商品庫又被表鎖卡死了!每秒 500 單變 5 單!"
"小林,數據庫鎖表了,MyISAM 的表鎖就是定時炸彈!我們要突破技術封鎖,開發一套劃時代的存儲引擎“ CTO 對小林說道。
生在 2025 年作為互聯網打工牛馬的林淵,學過很多關于 MySQL 的技術,記憶如潮水涌入——2025 年的 InnoDB 架構圖在他腦中展開,InnoDB 內存架構、磁盤架構;
以及那些 Buffer Pool、Change Buffer 的代碼如同梵高星月夜般絢爛,解鎖《InnoDB 設計圖鑒》,準備揚名立萬。
于是他在京都國際數據庫提交一篇提案:《論行級鎖與內存緩沖池——下一代存儲引擎設計提案》,附件性能對比圖震撼業界。
場景 | MyISAM | "InnoDB"原型 |
100 萬并發更新 | 崩潰 | TPS 18,492 |
范圍查詢 | 12.8s | 0.3s |
InnoDB 內存結構主要包含 Buffer Pool 、Change Buffer 、Adaptive Hash Index (自適應哈希索引)和 Log Buffer。
Buffer Pool
Buffer Pool 是主內存中的一個區域,它在訪問時緩存表和索引數據。Buffer Pool 允許頻繁使用的數據直接從內存中訪問,從而加快處理速度。
Buffer Pool 是 InnoDB 引擎的核心內存組件,采用預分配的連續內存空間,默認大小通過 innodb_buffer_pool_size
配置(建議設置為物理內存的 60-80%)。
其本質是一個基于頁(Page)的緩存系統,通過 Page Directory 和 Free List 實現高效內存管理。
為了提高大量讀取操作的效率,緩沖池被劃分為可以潛在地包含多行的頁面。
Buffer Pool LRU 算法
Buffer Pool 使用 LRU 算法的變體進行管理。當需要空間向緩沖池中添加新頁面時,最近最少使用的頁面被移除,并將新頁面添加到列表的中間。
這種中間插入策略將列表視為兩個子列表:
- 在頭部,一個包含最近訪問的新(“young”)頁面的子列表,稱之為 「New Sublist」
- 在尾部,一個包含較舊(“Old”)頁面的子列表,稱之為 「Old Sublist」,這些頁面數據通常是較少被訪問的。
圖片
算法將頻繁使用的頁面保留在「New Sublist」中?!窸ld Sublist」包含使用頻率較低的頁面。
默認情況下,算法按以下方式運行:
- Buffer Pool 的 3/8 用于「Old Sublist」。
- 列表的中點是「New Sublist」的尾部與「Old Sublist」的頭部相交的邊界。
- 當
InnoDB
將頁面讀入 Buffer Pool 時,它最初將其插入中點(「Old Sublist」的頭部)。可以讀取頁面,因為它需要用于用戶發起的操作,如 SQL 查詢,或者作為InnoDB
自動執行的預讀操作的組成部分。 - 訪問「Old Sublist」中的頁面數據會將其設置為“Young”,將其移動到「New Sublist」的頭部。如果頁面是因為用戶發起的操作而讀取的,則第一次訪問立即發生,頁面變為“Young”。如果頁面是因為預讀操作而讀取的,則第一次訪問不會立即發生。
- 隨著數據庫的運行,Buffer Pool 中未被訪問的頁面會通過向列表尾部移動而“老化”?!窷ew Sublist」和「Old Sublist」中的頁面都會隨著其他頁面變為新頁面而老化。當頁面在中間插入時,「Old Sublist」中的頁面也會老化。最終,一個未被使用的頁面會到達「Old Sublist」的尾部并被淘汰。
優化提示:應將緩沖池的大小設置為盡可能大的值,同時為服務器上其他進程運行留出足夠的內存,避免過度分頁。緩沖池越大,
InnoDB
就越像內存數據庫,一次從磁盤讀取數據,然后在后續讀取中從內存中訪問數據。
Change Buffer
"每秒 10 萬次非主鍵更新,磁盤 IOPS 爆表!"運維總監癱坐在監控屏前。林淵拔下服務器電源:"上 Change Buffer!"
實時監控屏上,磁盤寫入曲線從鋸齒狀驟變為平滑直線:
Before: IOPS 15,000 → After: 2,300 (下降85%)
甲骨文特派員 Mike 臉色鐵青:"這算法...至少超越時代十年!”
林淵清了清嗓子,繼續給大家解釋 Change Buffer 的設計思路……
Change Buffer 是一種特殊的數據結構,針對非唯一二級索引的寫優化結構,用于緩存當二級索引頁不在 Buffer Pool 中時的寫操作。
當二級索引頁不在緩沖池中時,用于緩存對這些頁面的更改。這些由 INSERT
、 UPDATE
或 DELETE
操作(DML)產生的緩沖更改,將在頁面通過其他讀取操作加載到緩沖池時進行合并。
- 若目標頁不在 Buffer Pool,將變更記錄寫入 Change Buffer 生成 Redo Log 保證持久化;
- 當后續讀取該索引頁時,將 Change Buffer 中的變更合并(Merge)到 Buffer Pool,觸發異步刷盤。
圖片
臺下有人問:“有了 Buffer Pool 為何還要再設計一個 Change Buffer 呢?”
與聚簇索引不同,二級索引通常是非唯一的,且對二級索引的插入操作往往以相對隨機的順序發生。
同樣,刪除和更新操作也可能影響索引樹中不相鄰的二級索引頁。
當其他操作將受影響的頁讀入 Buffer Pool 時,隨后將緩存的更改合并,避免了從磁盤讀取二級索引頁至 Buffer Pool 所需的大量隨機訪問 I/O。
系統在空閑時段或緩慢關閉期間運行的清除操作會周期性地將更新的索引頁寫入磁盤。
相較于立即逐條寫入磁盤,清除操作能以更高效的方式批量寫入包含連續索引值的磁盤塊。
Change Buffer 有什么不足呢?
當存在大量受影響的行和需要更新的二級索引時,Change Buffer 合并可能需要數小時。
在此期間,磁盤 I/O 會增加,可能導致磁盤密集型查詢顯著變慢。Change Buffer 合并操作可能在事務提交后持續進行,甚至在服務器關閉并重啟后仍會繼續。
在內存中,Change Buffer 占用 Buffer Pool 的一部分空間。在磁盤上,Change Buffer 屬于系統表空間的一部分,當數據庫服務器關閉時,索引變更將在此處緩沖存儲。
Change Buffer 劃時代意義
當對表執行 INSERT
、 UPDATE
和 DELETE
操作時,索引列的值(尤其是二級鍵的值)通常處于無序狀態,需要大量 I/O 操作來更新二級索引。
當相關頁面不在 Buffer Pool 中時,Change Buffer 會緩存對二級索引條目的修改,從而避免立即從磁盤讀取頁面所產生的高昂 I/O 開銷。
當頁面被加載到 Buffer Pool 時,緩沖的更改會被合并,更新后的頁面隨后會刷寫到磁盤。
由于變更緩沖能夠減少磁盤讀寫次數,因此對于 I/O 密集型工作負載(例如涉及大量 DML 操作的應用場景,如批量插入)具有重要價值,這類場景可顯著受益于 Change Buffer 機制。
Adaptive Hash Index(自適應哈希索引)
2005 年 eBay 中國競標現場,林淵與 Oracle 團隊正面對決。
"貴司方案處理不了熱點數據吧?"Oracle 首席亮出 TPC-C 測試報告。林淵輕笑一聲,敲下:
SET GLOBAL innodb_adaptive_hash_index=ON;
-- 激活哈希索引
瞬間,用戶 ID 查詢從 378ms 降至 0.09ms。
自適應哈希索引(Adaptive Hash Index,AHI) 是 InnoDB 存儲引擎內部自動創建和管理的哈希索引,用于優化 等值查詢(如 WHERE key = 'value'
) 的性能。
與傳統手動創建的哈希索引不同,AHI 完全由 InnoDB 根據查詢模式動態生成和銷毀,無需用戶干預。
核心作用:通過將頻繁訪問的索引鍵值映射到哈希表,繞過 B+ 樹的逐層查找,直接定位到目標數據頁,從而減少磁盤 I/O 和 CPU 開銷。
他的觸發條件是什么?
InnoDB 通過監控索引頁的訪問模式,動態決定是否創建 AHI:
- 頻率閾值:同一索引頁被連續訪問超過
100 次
。 - 查詢模式匹配:相同查詢條件多次訪問同一頁(次數閾值:
頁中記錄數 / 16
)。
生命周期管理
- 自動創建:滿足觸發條件時動態生成哈希條目。
- 自動淘汰:
當索引頁不再被頻繁訪問時,通過 LRU 機制逐步移除哈希條目。
當表被刪除或重建時,相關 AHI 條目自動清理。
工作流程如下圖所示:
圖片
優缺點分析
優點 | 局限性 |
減少等值查詢的 B+ 樹遍歷層級 | 僅適用于等值查詢(=, IN),不適用范圍查詢 |
降低 CPU 和 I/O 開銷 | 哈希沖突可能影響性能 |
完全自動化,無需人工維護 | 高并發場景可能因鎖爭用成為瓶頸 |
對熱點數據訪問有顯著加速效果 | 內存占用增加(需權衡 |
使用場景建議
- 推薦開啟: OLTP 系統中以等值查詢為主的場景(如用戶中心、訂單查詢)。
- 建議關閉:
寫密集型負載(如日志寫入)。
內存緊張或出現大量哈希沖突時。
使用 SSD 且 Buffer Pool 足夠大時,B+ 樹自身性能已足夠。
Log Buffer(日志緩沖區)
"林工,交易系統每秒百萬事務,如何保證零丟失?"
用 Redo Log 實現!避免每次事務操作都寫磁盤,我設計了 Log Buffer。
Log Buffer 是 InnoDB 存儲引擎用于臨時緩存 重做日志(Redo Log) 的內存區域。
Log Buffer 大小由 innodb_log_buffer_size
變量定義,默認大小為 64MB。
所有事務對數據的修改在寫入磁盤前,其對應的 Redo Log 會先寫入 Log Buffer,隨后按策略批量刷新到磁盤的 Redo Log 文件中。
設計目標:
- 減少磁盤 I/O 次數:合并多個日志寫入操作,避免頻繁的小數據量磁盤寫入。
- 提升事務響應速度:延遲日志刷盤,降低事務提交的等待時間。
- 保證持久性(Durability):通過可控的刷盤策略,確保已提交事務的日志最終持久化。
Log Buffer 內容會定期刷新到磁盤。較大的 Log Buffer 允許大型事務在提交前無需將 Redo Log 數據寫入磁盤。
因此,若有更新、插入或刪除大量行的事務,增大 Log Buffer 可節省磁盤 I/O。
Log Buffer 實現原理
Log Buffer 內存結構與寫入流程:
- 日志生成:
- 事務修改數據頁時,生成 Redo Log 記錄。
- 日志記錄包含修改內容、LSN(Log Sequence Number)等信息。
- 緩沖區寫入:
Log Buffer 空間足夠,則日志按順序追加到 Log Buffer 的空閑位置。
使用
buf_free
指針標記當前寫入偏移量。
刷盤觸發條件:
事務提交:根據
innodb_flush_log_at_trx_commit
設置決定是否刷盤。緩沖區滿:當寫入數據超過緩沖區空閑空間時強制刷盤。
定時任務:每隔
innodb_flush_log_at_timeout
秒觸發刷盤(策略為 0 或 2 時)。
圖片
刷盤策略詳解
參數值 | 行為描述 | 數據安全性 | 性能 | 適用場景 |
0 | 日志每秒刷盤一次,事務提交時不強制刷盤 | 最低 | 最高 | 非關鍵數據批量處理 |
1 | 每次事務提交時同步刷盤(fsync) | 最高 | 最低 | 金融交易等高安全需求 |
2 | 事務提交時寫入操作系統緩存,不立即刷盤;每秒由操作系統異步刷盤 | 中等 | 較高 | 常規業務系統 |
Log Buffer 與 Redo Log 協作
1. 與 Redo Log 文件的關系
- 循環寫入:Redo Log 文件(如
ib_logfile0
、ib_logfile1
)以循環方式復用。 - LSN 協調:
每個日志記錄攜帶 LSN,全局唯一且遞增。
Checkpoint LSN 標記已持久化的日志位置。
2. 崩潰恢復流程
圖片
- 重啟時掃描 Redo Log:從最后一個 Checkpoint LSN 開始重放日志。
- 前滾(Redo):將 Log Buffer 中未刷盤的日志重新應用到數據頁。
- 后滾(Undo):通過 Undo Log 回滾未提交的事務。
3. Group Commit 優化
圖片
- 合并提交:多個事務的日志寫入合并為一次磁盤操作。
- 工作流程:
- 事務提交時,將日志追加到 Log Buffer。
- 由后臺線程統一將多個事務的日志批量寫入磁盤。
- 減少頻繁的
fsync
調用,提升高并發下的吞吐量。
Log Buffer 通過內存緩沖和批量刷盤機制,在 事務持久性 與 系統性能 之間取得平衡。
合理配置 innodb_flush_log_at_trx_commit
和緩沖區大小,結合 Group Commit 等優化技術,可顯著提升高并發場景下的數據庫性能。
同時,需根據業務容忍度選擇恰當的刷盤策略,避免數據丟失風險。