淺析 InnoDB 存儲引擎的工作流程源碼分析

InnoDB

InnoDB 是由 Innobase Oy 公司開發(fā),該存儲引擎是第一個完整支持 ACID 事務(wù)的 MySQL 存儲引擎。具有插入緩存、兩次寫、自適應(yīng)哈希索引等關(guān)鍵特性,是一個高性能、高可用的存儲引擎。

整體架構(gòu)

InnoDB 有多個內(nèi)存塊,這些內(nèi)存塊組合在一起組成了一個大的內(nèi)存池。而 InnoDB 的內(nèi)存池中會有多個后臺線程,這些后臺線程負責刷新內(nèi)存池中的數(shù)據(jù),和將臟頁(已修改的數(shù)據(jù)頁)刷新到磁盤文件。關(guān)注、轉(zhuǎn)發(fā)、評論頭條號每天分享java知識,私信回復“555”贈送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式資料

后臺線程

默認情況下,InnoDB 存儲引擎有 13 個后臺線程:

一個 master 線程

一個鎖監(jiān)控線程

一個錯誤監(jiān)控線程

十個 IO 線程

插入緩存線程

日志線程

讀線程(默認 4 個)

寫線程(默認 4 個)

下面是我本機上的十個 IO 線程

--------

FILE I/O

--------

I/O thread 0 state: waiting for i/o request (insert buffer thread)

I/O thread 1 state: waiting for i/o request (log thread)

I/O thread 2 state: waiting for i/o request (read thread)

I/O thread 3 state: waiting for i/o request (read thread)

I/O thread 4 state: waiting for i/o request (read thread)

I/O thread 5 state: waiting for i/o request (read thread)

I/O thread 6 state: waiting for i/o request (write thread)

I/O thread 7 state: waiting for i/o request (write thread)

I/O thread 8 state: waiting for i/o request (write thread)

I/O thread 9 state: waiting for i/o request (write thread)

Pending normal aio reads: 0 [0, 0, 0, 0] , aio writes: 0 [0, 0, 0, 0] ,

ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0

Pending flushes (fsync) log: 0; buffer pool: 0

540 OS file reads, 89 OS file writes, 7 OS fsyncs

0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s

內(nèi)存池

InnoDB 存儲引擎的內(nèi)存池包含:緩沖池、日志緩存池、額外內(nèi)存池。這些內(nèi)存的大小分別由配置文件中的參數(shù)決定。其中占比最大的是緩沖池,里面包含了數(shù)據(jù)緩存頁、索引、插入緩存、自適應(yīng)哈希索引、鎖信息和數(shù)據(jù)字典。InnoDB 會在讀取數(shù)據(jù)庫數(shù)據(jù)的時候,將數(shù)據(jù)緩存到緩沖池中,而在修改數(shù)據(jù)的時候,會先把緩沖池中的數(shù)據(jù)修改掉,一旦修改過的數(shù)據(jù)頁就會被標記為臟頁,而臟頁則會被 master 線程按照一定的頻率刷新到磁盤中。日志緩存則是緩存了redo-log 信息,然后再刷新到 redo-log 文件中。額外內(nèi)存池則是在對一些數(shù)據(jù)結(jié)構(gòu)本身分配內(nèi)存時會從額外內(nèi)存池中申請內(nèi)存,當該區(qū)域內(nèi)存不足則會到緩沖池中申請。

Master Thread

InnoDB 存儲引擎的主要工作都在一個單獨的 Master Thread 中完成,其內(nèi)部由四個循環(huán)體構(gòu)成:主循環(huán)( loop )、后臺循環(huán)( background loop )、刷新循環(huán)( flush loop )、暫停循環(huán)( suspend loop )。具體工作流程如下圖所示:

主循環(huán)

主要負責將緩沖池中的日志文件刷新到磁盤中、合并插入緩存、刷新緩沖池中的臟頁數(shù)據(jù)到磁盤中、刪除無用的 Undo 頁、產(chǎn)生一個 checkpoint 。在主循環(huán)中會多次將臟頁刷新到磁盤中,但是有一些刷新任務(wù)總會執(zhí)行,有一些則根據(jù)參數(shù)來判斷當前是否需要刷新。而這個參數(shù) innodb_max_dirty_pages_pct 最大臟頁比例是通過配置文件決定的,你可以根據(jù)實際情況來調(diào)整你自己的最大臟頁比例,來達到最好的性能。

偽代碼如下:

for (int i = 0; i<10; i++) {

thread_sleep(1)

do log buffer flush to disk

if ( last_one_second_ios < 5) {

do merge at most 5 insert buffer

}

if (buf_get_modified_ratio_pct > innodb_max_ditry_pages_pct) {

do buffer pool flush 100 dirty page

}

if (no user activity) {

goto background loop

}

}

if (last_ten_second_ios < 200) {

do buffer pool flush 100 dirty page

}

do merge at most 5 insert buffer

do log buffer flush to disk

do full pourge

if (buf_get_modifued_ratio_pct > 70%) {

do buffer pool flush 100 dirty page

} else {

buffer pool flush 10 dirty page

}

do fuzzy checkpoint

goto loop

后臺循環(huán)

在后臺循環(huán)中 InnoDB 會做這些事:刪除無用的Undo頁、合并插入緩存。如果當前 InnoDB 處于空閑狀態(tài),則跳轉(zhuǎn)到刷新循環(huán),否則跳轉(zhuǎn)到主循環(huán)繼續(xù)處理數(shù)據(jù)。

偽代碼如下:

do full purge

do merge 20 insert buffer

if (not idle) {

goto loop

} else {

goto flush loop

}

刷新循環(huán)

一旦執(zhí)行到刷新循環(huán),InnoDB 會一直處理臟頁數(shù)據(jù),直到臟頁數(shù)據(jù)達到最大臟頁比例以下。這時候會跳轉(zhuǎn)到暫停循環(huán)中(所有數(shù)據(jù)都處理完畢)。

偽代碼如下:

flush loop:

do buffer pool flush 100 dirty page

if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct) {

goto flush loop

} else {

goto suspend loop

}

暫停循環(huán)

在本循環(huán)中,InnoDB會將 Master Thread 掛起,減少內(nèi)存資源使用,一直處于 waiting 狀態(tài),等待事件來喚醒。一旦有新的事件過來,就跳轉(zhuǎn)到主循環(huán)中。

偽代碼如下:

suspend loop:

suspend_thread()

waiting event

goto loop;

由此可以看出,master 線程的最大的工作內(nèi)容就是刷新臟頁數(shù)據(jù)到磁盤了。這一步就是把緩存池中被修改的數(shù)據(jù)頁同步到磁盤中。而臟頁數(shù)據(jù)的刷新基本上都是由innodb_max_dirty_pages_pct 來控制的,所以當你的服務(wù)器處理能力比較強,給 InnoDB 分配的內(nèi)存池比較大,這時候可能你的臟頁數(shù)據(jù)會很難達到最大臟頁比,這時候你的數(shù)據(jù)基本上都在緩沖池中,可能需要很長一段時間才會到數(shù)據(jù)庫磁盤文件中,也就是臟頁的刷新速度會很低(MySQL 5.1之前的版本默認是 90%,后面調(diào)整到 75%)。所以實際應(yīng)用中可以根據(jù)自己內(nèi)存和數(shù)據(jù)庫的讀寫量來設(shè)置這個最大臟頁比。對于一次刷新臟頁數(shù)量的設(shè)置,在 InnoDB Plugin 中有一個參數(shù) innodb_adaptive_flushing自適應(yīng)刷新,InnoDB 會根據(jù)產(chǎn)生的重做日志速度來計算出當前最適合的刷新臟頁數(shù)量。當然 InnoDB Plugin 中還有其它很多參數(shù)配置,合理利用這些配置可以極大的提升 InnoDB 存儲引擎的性能。

關(guān)鍵特性

前面說到 InnoDB 的三大特性分別為:插入緩存、兩次寫、自適應(yīng)哈希索引。下面就簡單介紹下這三大特性。

插入緩存

當我插入一條數(shù)據(jù),該數(shù)據(jù)只有一個 ID 索引(聚集索引:數(shù)據(jù)行的物理順序與列值的邏輯順序相同)的時候,并且 ID 是自增長的,這時候頁中的行記錄按照 ID 順序存放,所以只需要在最新頁插入數(shù)據(jù)即可。但是如果我的表有多個非聚集索引(該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同),在插入的時候非聚集索引的插入不再是順序的,這時候要離散的訪問非聚集索引頁,導致插入性能變低。而插入緩存則在插入的時候判斷緩沖池中是否存在當前非聚集索引,如果存在則直接插入,否則先插入到一個緩存區(qū),然后再通過 Master Thread 來合并插入緩存。這樣極大的提高了數(shù)據(jù)的寫性能。

兩次寫

兩次寫是為了解決在將緩沖池中的臟頁刷新到磁盤的過程中,操作系統(tǒng)出現(xiàn)故障,導致當前的臟頁部分寫失效的問題。通過兩次寫在下次恢復的時候,InnoDB 會根據(jù)兩次寫的結(jié)果來恢復數(shù)據(jù)。

原理:在刷新臟頁的時候,不是直接把臟頁數(shù)據(jù)刷新到磁盤,而是將臟頁先寫到一個大小為2M的內(nèi)存緩存中,再將這個內(nèi)存緩存數(shù)據(jù)同步到磁盤的共享表空間中。當全部都寫到共享表空間后,再將數(shù)據(jù)刷新到磁盤中。這樣如果發(fā)生了上面描述的情況,這時候數(shù)據(jù)會在共享表空間中有個備份,恢復的時候就可以使用共享表空間的數(shù)據(jù)。

如果有數(shù)據(jù)庫集群的情況下,master數(shù)據(jù)庫是一定要開啟兩次寫的,為了保證數(shù)據(jù)可靠性。而從數(shù)據(jù)庫可以通過參數(shù) skip_innodb_doublewrite 來禁止兩次寫功能,來提高插入效率。

自適應(yīng)哈希索引

InnoDB 會監(jiān)控對表示的索引查找,如果發(fā)現(xiàn)可以通過對索引進行哈希來優(yōu)化搜索。這時候會對當前的索引建立哈希索引。稱之為自適應(yīng)哈希索引( AHI )。可以通過參數(shù)innodb_adaptive_hash_index 來禁用或啟用此特性。

小結(jié)

總體來說 InnoDB 的高性能體現(xiàn)在:插入數(shù)據(jù)的時候先保存在內(nèi)存中,直接跟內(nèi)存交互性能比較好,而且還有插入緩存優(yōu)化,保證了高并發(fā)寫操作。高可用則表現(xiàn)在兩次寫特性,保證了機器宕機或者出故障的時候數(shù)據(jù)不會丟失。這里只是簡單介紹了一下 InnoDB 的工作流程和一些特性,當然 InnoDB 還有很多很多強大的功能,比如說事務(wù)、鎖、索引、算法等等

 在此我向大家推薦一個架構(gòu)學習交流群。交流學習群號:938837867 暗號:555 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容