久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

mysql體系結(jié)構(gòu)和InnoDB存儲(chǔ)引擎知識(shí)有哪些

共計(jì) 17485 個(gè)字符,預(yù)計(jì)需要花費(fèi) 44 分鐘才能閱讀完成。

這篇文章主要介紹“mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些”,在日常操作中,相信很多人在 mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些”的疑惑有所幫助!接下來,請(qǐng)跟著丸趣 TV 小編一起來學(xué)習(xí)吧!

MySQL 基本架構(gòu)圖

大體來說,MySQL 可以分為 Server 層和存儲(chǔ)引擎層兩部分。

Server 層包括連接器、查詢緩存、分析器、優(yōu)化器、執(zhí)行器等,涵蓋 MySQL 的大多數(shù)核心服務(wù)功能,以及所有的內(nèi)置函數(shù)(如日期、時(shí)間、數(shù)學(xué)和加密函數(shù)等),所有跨存儲(chǔ)引擎的功能都在這一層實(shí)現(xiàn),比如存儲(chǔ)過程、觸發(fā)器、視圖等。

連接器

連接器就是你連接到數(shù)據(jù)庫(kù)時(shí)使用的,負(fù)責(zé)跟客戶端建立連接、獲取權(quán)限、維持和管理連接。

命令:mysql -h$ip -P$port -u$user -p,回車后輸密碼,也可以在 -p 后面輸入密碼,但是有密碼泄露的風(fēng)險(xiǎn)。

show processlist, 可以查看連接的情況,Command 列中有一個(gè) Sleep 表示連接空閑。

空閑連接默認(rèn) 8 小時(shí)會(huì)被斷開,可以由 wait_timeout 參數(shù)配置。

在數(shù)據(jù)庫(kù)中,長(zhǎng)連接是指連接成功后,如果客戶端持續(xù)有請(qǐng)求,則一直使用同一個(gè)連接。短連接則是指每次執(zhí)行完很少的幾次查詢就斷開連接,下次查詢?cè)僦匦陆⒁粋€(gè)。

由于建立連接比較耗資源,所以建議盡量使用長(zhǎng)連接,但是使用長(zhǎng)連接后,MySQL 占用內(nèi)存漲得特別快,這是因?yàn)?MySQL 在執(zhí)行過程中臨時(shí)使用的內(nèi)存是管理在連接對(duì)象里面的。這些資源會(huì)在連接斷開的時(shí)候才釋放。所以如果長(zhǎng)連接累積下來,可能導(dǎo)致內(nèi)存占用太大,被系統(tǒng)強(qiáng)行殺掉(OOM),從現(xiàn)象看就是 MySQL 異常重啟了。

解決方案:

定期斷開長(zhǎng)連接。使用一段時(shí)間,或者程序里面判斷執(zhí)行過一個(gè)占用內(nèi)存的大查詢后,斷開連接,之后要查詢?cè)僦剡B。

如果你用的是 MySQL 5.7 或更新版本,可以在每次執(zhí)行一個(gè)比較大的操作后,通過執(zhí)行 mysql_reset_connection 來重新初始化連接資源。這個(gè)過程不需要重連和重新做權(quán)限驗(yàn)證,但是會(huì)將連接恢復(fù)到剛剛創(chuàng)建完時(shí)的狀態(tài)。

查詢緩存

查詢緩存是將之前執(zhí)行過的語(yǔ)句及其結(jié)果以 key-value 對(duì)的形式緩存在內(nèi)存中。key 是查詢的語(yǔ)句,value 是查詢的結(jié)果。如果你的查詢能夠直接在這個(gè)緩存中找到 key,那么這個(gè) value 就會(huì)被直接返回給客戶端。

查詢緩存在 MYSQL8 時(shí)被移除了,由于查詢緩存失效頻繁,命中率低。

分析器

分析器先會(huì)做“詞法分析”,識(shí)別出里面的字符串分別是什么,代表什么。然后需要做“語(yǔ)法分析”,判斷你輸入的這個(gè) SQL 語(yǔ)句是否滿足 MySQL 語(yǔ)法。

優(yōu)化器

執(zhí)行器

存儲(chǔ)引擎層負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和提取。其架構(gòu)模式是插件式的,支持 InnoDB、MyISAM、Memory 等多個(gè)存儲(chǔ)引擎。現(xiàn)在最常用的存儲(chǔ)引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成為了默認(rèn)存儲(chǔ)引擎。

一條 Select 語(yǔ)句執(zhí)行流程

上圖以 InnoDB 存儲(chǔ)引擎為例,處理過程如下:

用戶發(fā)送請(qǐng)求到 tomcat,通過 tomcat 鏈接池和 mysql 連接池建立連接,然后通過連接發(fā)送 SQL 語(yǔ)句到 MySQL;

MySQL 有一個(gè)單獨(dú)的監(jiān)聽線程,讀取到請(qǐng)求數(shù)據(jù),得到連接中請(qǐng)求的 SQL 語(yǔ)句;

將獲取到的 SQL 數(shù)據(jù)發(fā)送給 SQL 接口去執(zhí)行;

SQL 接口將 SQL 發(fā)送給 SQL 解析器進(jìn)行解析;

將解析好的 SQL 發(fā)送給查詢優(yōu)化器,找到最優(yōu)的查詢路勁,然后發(fā)給執(zhí)行器;

執(zhí)行器根據(jù)優(yōu)化后的執(zhí)行方案調(diào)用存儲(chǔ)引擎的接口按照一定的順序和步驟進(jìn)行執(zhí)行。

舉個(gè)例子,比如執(zhí)行器可能會(huì)先調(diào)用存儲(chǔ)引擎的一個(gè)接口,去獲取“users”表中的第一行數(shù)據(jù),然后判斷一下這個(gè)數(shù)據(jù)的“id”字段的值是否等于我們期望的一個(gè)值,如果不是的話,那就繼續(xù)調(diào)用存儲(chǔ)引擎的接口,去獲取“users”表的下一行數(shù)據(jù)。就是基于上述的思路,執(zhí)行器就會(huì)去根據(jù)我們的優(yōu)化器生成的一套執(zhí)行計(jì)劃,然后不停的調(diào)用存儲(chǔ)引擎的各種接口去完成 SQL 語(yǔ)句的執(zhí)行計(jì)劃,大致就是不停的更新或者提取一些數(shù)據(jù)出來。

在這里涉及到幾個(gè)問題:

MySQL 驅(qū)動(dòng)到底是什么東西?

以 java 為例,我們們?nèi)绻?Java 系統(tǒng)中去訪問一個(gè) MySQL 數(shù)據(jù)庫(kù),必須得在系統(tǒng)的依賴中加入一個(gè) MySQL 驅(qū)動(dòng),比如在 Maven 里面要加上

dependency 
  groupId mysql /groupId 
  artifactId mysql-connector-java /artifactId 
  version 5.1.46 /version 
 /dependency

那么這個(gè) MySQL 驅(qū)動(dòng)到底是個(gè)什么東西? 其實(shí) L 驅(qū)動(dòng)就會(huì)在底層跟數(shù)據(jù)庫(kù)建立網(wǎng)絡(luò)連接,有網(wǎng)絡(luò)連接,接著才能去發(fā)送請(qǐng)求給數(shù)據(jù)庫(kù)服務(wù)器!讓語(yǔ)言編寫的系統(tǒng)通過 MySQL 驅(qū)動(dòng)去訪問數(shù)據(jù)庫(kù),如下圖

數(shù)據(jù)庫(kù)連接池到底是用來干什么的?

假設(shè)用 java 開發(fā)一個(gè) web 服務(wù)部署在 tomcat 上,tomcat 可以多線程并發(fā)處理請(qǐng)求,所以首先一點(diǎn)就是不可能只會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接(多個(gè)請(qǐng)求去搶一個(gè)連接,效率得多低下)。

其次,如果每個(gè)請(qǐng)求都去創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接呢? 這也是非常不好的,因?yàn)槊看谓⒁粋€(gè)數(shù)據(jù)庫(kù)連接都很耗時(shí),好不容易建立好了連接,執(zhí)行完了 SQL 語(yǔ)句,還把數(shù)據(jù)庫(kù)連接給銷毀,頻繁創(chuàng)建和銷毀帶來性能問題。

所以一般使用數(shù)據(jù)庫(kù)連接池,也就是在一個(gè)池子里維持多個(gè)數(shù)據(jù)庫(kù)連接,讓多個(gè)線程使用里面的不同的數(shù)據(jù)庫(kù)連接去執(zhí)行 SQL 語(yǔ)句,然后執(zhí)行完 SQL 語(yǔ)句之后,不要銷毀這個(gè)數(shù)據(jù)庫(kù)連接,而是把連接放回池子里,后續(xù)還可以繼續(xù)使用。基于這樣的一個(gè)數(shù)據(jù)庫(kù)連接池的機(jī)制,就可以解決多個(gè)線程并發(fā)的使用多個(gè)數(shù)據(jù)庫(kù)連接去執(zhí)行 SQL 語(yǔ)句的問題,而且還避免了數(shù)據(jù)庫(kù)連接使用完之后就銷毀的問題了。

MySQL 數(shù)據(jù)庫(kù)的連接池是用來干什么的?

MySQL 數(shù)據(jù)庫(kù)的連接池的作用和 java 應(yīng)用端連接池作用一樣,都是起到了復(fù)用連接的作用。

InnoDB 存儲(chǔ)引擎

InnoDB 架構(gòu)簡(jiǎn)析

從圖中可見,InnoDB 存儲(chǔ)引擎由內(nèi)存池,后臺(tái)線程和磁盤文件三大部分組成

再來一張突出重點(diǎn)的圖:

InnoDB 存儲(chǔ)引擎第一部分:內(nèi)存結(jié)構(gòu)

Buffer Pool 緩沖池

InnoDB 存儲(chǔ)引擎基于磁盤存儲(chǔ)的,并將其中的記錄按照頁(yè)的方式進(jìn)行管理,但是由于 CPU 速度和磁盤速度之間的鴻溝,基于磁盤的數(shù)據(jù)庫(kù)系統(tǒng)通常使用緩沖池記錄來提高數(shù)據(jù)庫(kù)的整體性能。

在數(shù)據(jù)庫(kù)進(jìn)行讀取操作,將從磁盤中讀到的頁(yè)放在緩沖池中,下次再讀取相同的頁(yè)中時(shí),首先判斷該頁(yè)是否在緩沖池中。若在緩沖池中,稱該頁(yè)在緩沖池中被命中,直接讀取該頁(yè),否則讀取磁盤上的頁(yè)。

對(duì)于數(shù)據(jù)庫(kù)中頁(yè)的修改操作,首先修改在緩沖池中的頁(yè),然后再以一定的頻率刷新到磁盤上,頁(yè)從緩沖池刷新回磁盤的操作并不是在每次頁(yè)發(fā)生更新時(shí)觸發(fā),而是通過一種稱為 CheckPoint 的機(jī)制刷新回磁盤。所以,緩沖池的大小直接影響著數(shù)據(jù)庫(kù)的整體性能,可以通過配置參數(shù) innodb_buffer_pool_size 來設(shè)置,緩沖池默認(rèn)是 128MB,還是有點(diǎn)小的,如果你的數(shù)據(jù)庫(kù)是 16 核 32G 的機(jī)器,那么你就可以給 Buffer Pool 分配個(gè) 2GB 的內(nèi)存。

由于緩沖池不是無限大的,隨著不停的把磁盤上的數(shù)據(jù)頁(yè)加載到緩沖池中,緩沖池總要被用完,這個(gè)時(shí)候只能淘汰掉一些緩存頁(yè),淘汰方式就使用最近最少被使用算法(LRU),具體來說就是引入一個(gè)新的 LRU 鏈表,通過這個(gè) LRU 鏈表,就可以知道哪些緩存頁(yè)是最近最少被使用的,那么當(dāng)你緩存頁(yè)需要騰出來一個(gè)刷入磁盤的時(shí)候,可以選擇那個(gè) LRU 鏈表中最近最少被使用的緩存頁(yè)淘汰。

緩沖池中緩存的數(shù)據(jù)頁(yè)類型有:索引頁(yè)、數(shù)據(jù)頁(yè)、undo 頁(yè)、插入緩沖、自適應(yīng)哈希索引、InnoDB 存儲(chǔ)的鎖信息和數(shù)據(jù)字典信息。

數(shù)據(jù)頁(yè)和索引頁(yè)

頁(yè) (Page) 是 Innodb 存儲(chǔ)的最基本結(jié)構(gòu),也是 Innodb 磁盤管理的最小單位,與數(shù)據(jù)庫(kù)相關(guān)的所有內(nèi)容都存儲(chǔ)在 Page 結(jié)構(gòu)里。Page 分為幾種類型,數(shù)據(jù)頁(yè)和索引頁(yè)就是其中最為重要的兩種類型。

插入緩沖(Insert Buffer)

在 InnoDB 引擎上進(jìn)行插入操作時(shí),一般需要按照主鍵順序進(jìn)行插入,這樣才能獲取較高的插入性能。當(dāng)一張表中存在非聚簇的不唯一的索引時(shí),在插入時(shí),數(shù)據(jù)頁(yè)的存放還是按照主鍵進(jìn)行順序存放,但是對(duì)于非聚簇索引葉子節(jié)點(diǎn)的插入不再是順序的了,這時(shí)就需要離散的訪問非聚簇索引頁(yè),由于隨機(jī)讀取的存在導(dǎo)致插入操作性能下降。

所以 InnoDB 存儲(chǔ)引擎開創(chuàng)性地設(shè)計(jì)了 Insert Buffer,對(duì)于非聚集索引的插入或更新操作,不是每一次直接插入到索引頁(yè)中,而是先判斷插入的非聚集索引頁(yè)是否在緩沖池中,若在,則直接插入;若不在,則先放入到一個(gè) Insert Buffer 對(duì)象中,好似欺騙。數(shù)據(jù)庫(kù)這個(gè)非聚集的索引已經(jīng)插到葉子節(jié)點(diǎn),而實(shí)際并沒有,只是存放在另一個(gè)位置。然后再以一定的頻率和情況進(jìn)行 Insert Buffer 和輔助索引頁(yè)子節(jié)點(diǎn)的 merge(合并)操作,這時(shí)通常能將多個(gè)插入合并到一個(gè)操作中(因?yàn)樵谝粋€(gè)索引頁(yè)中),這就大大提高了對(duì)于非聚集索引插入的性能。

然而 Insert Buffer 的使用需要同時(shí)滿足以下兩個(gè)條件:

索引是輔助索引(secondary index ) ;

索引不是唯一(unique)的。

當(dāng)滿足以上兩個(gè)條件時(shí),InnoDB 存儲(chǔ)引擎會(huì)使用 Insert Buffer,這樣就能提高插入操作的性能了。不過考慮這樣一種情況: 應(yīng)用程序進(jìn)行大量的插入操作,這些都涉及了不唯一的非聚集索引,也就是使用了 Insert Buffer。若此時(shí) MySQL 數(shù)據(jù)庫(kù)發(fā)生了宕機(jī)這時(shí)勢(shì)必有大量的 Insert Buffer 并沒有合并到實(shí)際的非聚集索引中去。

因此這時(shí)恢復(fù)可能需要很長(zhǎng)的時(shí)間, 在極端情況下甚至需要幾個(gè)小時(shí)。輔助索引不能是唯一的,因?yàn)樵诓迦刖彌_時(shí),數(shù)據(jù)庫(kù)并不去查找索引頁(yè)來判斷插入的記錄的唯一性。如果去查找肯定又會(huì)有離散讀取的情況發(fā)生,從而導(dǎo)致 Insert Buffer 失去了意義。

可以通過命令 SHOW ENGINE INNODB STATUS 來查看插入緩沖的信息

seg size 顯示了當(dāng)前 Insert Buffer 的大小為 11336×16KB, 大約為 177MB; free list len 代表了空閑列表的長(zhǎng)度;size 代表了已經(jīng)合并記錄頁(yè)的數(shù)量。而黑體部分的第 2 行可能是用戶真正關(guān)心的, 因?yàn)樗@示了插入性能的提高。Inserts 代表了插入的記錄數(shù);merged recs 代表了合并的插入記錄數(shù)量; merges 代表合并的次數(shù), 也就是實(shí)際讀取頁(yè)的次數(shù)。merges: merged recs 大約為 1:3, 代表了插入緩沖將對(duì)于非聚集索引頁(yè)的離散 IO 邏輯請(qǐng)求大約降低了 2 /3。

正如前面所說的, 目前 Insert Buffer 存在一個(gè)問題是: 在寫密集的情況下, 插入緩沖會(huì)占用過多的緩沖池內(nèi)存(innodb buffer pool), 默認(rèn)最大可以占用到 1 / 2 的緩沖池內(nèi)存。以下是 InnoDB 存儲(chǔ)引擎源代碼中對(duì)于 insert buffer 的初始化操作:

Change Buffer

InnoDB 從 1.0.x 版本開始引入了 Change Buffer, 可將其視為 Insert Buffer 的升級(jí)版本, InnodB 存儲(chǔ)引擎可以對(duì) DML 操作— INSERT、DELETE、UPDATE 都進(jìn)行緩沖, 他們分別是: Insert Buffer、Delete Buffer、Purge buffer 當(dāng)然和之前 Insert Buffer 一樣, Change Buffer 適用的對(duì)象依然是非唯一的輔助索引。

對(duì)一條記錄進(jìn)行 UPDATE 操作可能分為兩個(gè)過程:

將記錄標(biāo)記為已刪除;

真正將記錄刪除

因此 Delete Buffer 對(duì)應(yīng) UPDATE 操作的第一個(gè)過程, 即將記錄標(biāo)記為刪除。PurgeBuffer 對(duì)應(yīng) UPDATE 操作的第二個(gè)過程, 即將記錄真正的刪除。同時(shí), InnoDB 存儲(chǔ)引擎提供了參數(shù) innodb_change_buffering, 用來開啟各種 Buffer 的選項(xiàng)。該參數(shù)可選的值為: Inserts、deletes、purges、changes、all、none。Inserts、deletes、purges 就是前面討論過的三種情況。changes 表示啟用 Inserts 和 deletes,all 表示啟用所有,none 表示都不啟用。該參數(shù)默認(rèn)值為 all。

從 InnoDB1.2.x 版本開始, 可以通過參數(shù) innodb_change_buffer_max_size 來控制 Change Buffer 最大使用內(nèi)存的數(shù)量:

mysql  show variables like  innodb_change_buffer_max_size 
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25 |
+-------------------------------+-------+
1 row in set (0.05 sec)

innodb_change_buffer_max_size 值默認(rèn)為 25, 表示最多使用 1 / 4 的緩沖池內(nèi)存空間。

而需要注意的是, 該參數(shù)的最大有效值為 50 在 MySQL5.5 版本中通過命令 SHOW ENGINE INNODB STATUS, 可以觀察到類似如下的內(nèi)容:

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

可以看到這里顯示了 merged operations 和 discarded operation, 并且下面具體顯示 Change Buffer 中每個(gè)操作的次數(shù)。Insert 表示 Insert Buffer; delete mark 表示 Delete Buffer; delete 表示 Purge Buffer; discarded operations 表示當(dāng) Change Buffer 發(fā)生 merge 時(shí), 表已經(jīng)被刪除, 此時(shí)就無需再將記錄合并 (merge) 到輔助索引中了。

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

InnoDB 會(huì)根據(jù)訪問的頻率和模式,為熱點(diǎn)頁(yè)建立哈希索引,來提高查詢效率。InnoDB 存儲(chǔ)引擎會(huì)監(jiān)控對(duì)表上各個(gè)索引頁(yè)的查詢,如果觀察到建立哈希索引可以帶來速度上的提升,則建立哈希索引,所以叫做自適應(yīng)哈希索引。

自適應(yīng)哈希索引通過緩沖池的 B + 樹頁(yè)構(gòu)建而來,因此建立速度很快,而且不需要對(duì)整張數(shù)據(jù)表建立哈希索引。其有一個(gè)要求,即對(duì)這個(gè)頁(yè)的連續(xù)訪問模式必須一樣的,也就是說其查詢的條件必須完全一樣,而且必須是連續(xù)的。

鎖信息(lock info)

我們都知道,InnoDB 存儲(chǔ)引擎會(huì)在行級(jí)別上對(duì)表數(shù)據(jù)進(jìn)行上鎖,不過 InnoDB 打開一張表,就增加一個(gè)對(duì)應(yīng)的對(duì)象到數(shù)據(jù)字典。

數(shù)據(jù)字典

對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)、庫(kù)對(duì)象、表對(duì)象等的元信息的集合。在 MySQL 中,數(shù)據(jù)字典信息內(nèi)容就包括表結(jié)構(gòu)、數(shù)據(jù)庫(kù)名或表名、字段的數(shù)據(jù)類型、視圖、索引、表字段信息、存儲(chǔ)過程、觸發(fā)器等內(nèi)容,MySQL INFORMATION_SCHEMA 庫(kù)提供了對(duì)數(shù)據(jù)局元數(shù)據(jù)、統(tǒng)計(jì)信息、以及有關(guān) MySQL Server 的訪問信息(例如:數(shù)據(jù)庫(kù)名或表名,字段的數(shù)據(jù)類型和訪問權(quán)限等)。該庫(kù)中保存的信息也可以稱為 MySQL 的數(shù)據(jù)字典。

預(yù)讀機(jī)制

MySQL 的預(yù)讀機(jī)制,就是當(dāng)你從磁盤上加載一個(gè)數(shù)據(jù)頁(yè)的時(shí)候,他可能會(huì)連帶著把這個(gè)數(shù)據(jù)頁(yè)相鄰的其他數(shù)據(jù)頁(yè),也加載到緩存里去!

舉個(gè)例子,假設(shè)現(xiàn)在有兩個(gè)空閑緩存頁(yè),然后在加載一個(gè)數(shù)據(jù)頁(yè)的時(shí)候,連帶著把他的一個(gè)相鄰的數(shù)據(jù)頁(yè)也加載到緩存里去了,正好每個(gè)數(shù)據(jù)頁(yè)放入一個(gè)空閑緩存頁(yè)!

哪些情況下會(huì)觸發(fā) MySQL 的預(yù)讀機(jī)制?

有一個(gè)參數(shù)是 innodb_read_ahead_threshold,他的默認(rèn)值是 56,意思就是如果順序的訪問了一個(gè)區(qū)里的多個(gè)數(shù)據(jù)頁(yè),訪問的數(shù)據(jù)頁(yè)的數(shù)量超過了這個(gè)閾值,此時(shí)就會(huì)觸發(fā)預(yù)讀機(jī)制,把下一個(gè)相鄰區(qū)中的所有數(shù)據(jù)頁(yè)都加載到緩存里去。

如果 Buffer Pool 里緩存了一個(gè)區(qū)里的 13 個(gè)連續(xù)的數(shù)據(jù)頁(yè),而且這些數(shù)據(jù)頁(yè)都是比較頻繁會(huì)被訪問的,此時(shí)就會(huì)直接觸發(fā)預(yù)讀機(jī)制,把這個(gè)區(qū)里的其他的數(shù)據(jù)頁(yè)都加載到緩存里去這個(gè)機(jī)制是通過參數(shù) innodb_random_read_ahead 來控制的,他默認(rèn)是 OFF,也就是這個(gè)規(guī)則是關(guān)閉的。

所以默認(rèn)情況下,主要是第一個(gè)規(guī)則可能會(huì)觸發(fā)預(yù)讀機(jī)制,一下子把很多相鄰區(qū)里的數(shù)據(jù)頁(yè)加載到緩存里去。

預(yù)讀機(jī)制的好處為了提升性能。假設(shè)你讀取了數(shù)據(jù)頁(yè) 01 到緩存頁(yè)里去,那么接下來有可能會(huì)接著順序讀取數(shù)據(jù)頁(yè) 01 相鄰的數(shù)據(jù)頁(yè) 02 到緩存頁(yè)里去,這個(gè)時(shí)候,是不是可能在讀取數(shù)據(jù)頁(yè) 02 的時(shí)候要再次發(fā)起一次磁盤 IO?

所以為了優(yōu)化性能,MySQL 才設(shè)計(jì)了預(yù)讀機(jī)制,也就是說如果在一個(gè)區(qū)內(nèi),你順序讀取了好多數(shù)據(jù)頁(yè)了,比如數(shù)據(jù)頁(yè) 01 到數(shù)據(jù)頁(yè) 56 都被你依次順序讀取了,MySQL 會(huì)判斷,你可能接著會(huì)繼續(xù)順序讀取后面的數(shù)據(jù)頁(yè)。那么此時(shí)就提前把后續(xù)的一大堆數(shù)據(jù)頁(yè)(比如數(shù)據(jù)頁(yè) 57 到數(shù)據(jù)頁(yè) 72)都讀取到 Buffer Pool 里去。

緩沖池內(nèi)存管理

這里需要了解三個(gè)鏈表(Free List、Flush List、LRU List),

Free List 磁盤上的數(shù)據(jù)頁(yè)和緩存頁(yè)是一 一對(duì)應(yīng)起來的,都是 16KB,一個(gè)數(shù)據(jù)頁(yè)對(duì)應(yīng)一個(gè)緩存頁(yè)。數(shù)據(jù)庫(kù)會(huì)為 Buffer Pool 設(shè)計(jì)一個(gè) free 鏈表,他是一個(gè)雙向鏈表數(shù)據(jù)結(jié)構(gòu),這個(gè) free 鏈表里,每個(gè)節(jié)點(diǎn)就是一個(gè)空閑的緩存頁(yè)的描述數(shù)據(jù)塊的地址,也就是說,只要你一個(gè)緩存頁(yè)是空閑的,那么他的描述數(shù)據(jù)塊就會(huì)被放入這個(gè) free 鏈表中。剛開始數(shù)據(jù)庫(kù)啟動(dòng)的時(shí)候,可能所有的緩存頁(yè)都是空閑的,因?yàn)榇藭r(shí)可能是一個(gè)空的數(shù)據(jù)庫(kù),一條數(shù)據(jù)都沒有,所以此時(shí)所有緩存頁(yè)的描述數(shù)據(jù)塊,都會(huì)被放入這個(gè) free 鏈表中,除此之外,這個(gè) free 鏈表有一個(gè)基礎(chǔ)節(jié)點(diǎn),他會(huì)引用鏈表的頭節(jié)點(diǎn)和尾節(jié)點(diǎn),里面還存儲(chǔ)了鏈表中有多少個(gè)描述數(shù)據(jù)塊的節(jié)點(diǎn),也就是有多少個(gè)空閑的緩存頁(yè)。

Flush List 和 Free List 鏈表類似,flush 鏈表本質(zhì)也是通過緩存頁(yè)的描述數(shù)據(jù)塊中的兩個(gè)指針,讓被修改過的緩存頁(yè)的描述數(shù)據(jù)塊,組成一個(gè)雙向鏈表。凡是被修改過的緩存頁(yè),都會(huì)把他的描述數(shù)據(jù)塊加入到 flush 鏈表中去,flush 的意思就是這些都是臟頁(yè),后續(xù)都是要 flush 刷新到磁盤上去。

LRU List 由于緩沖池大小是一定的,換句話說 free 鏈表中的空閑緩存頁(yè)數(shù)據(jù)是一定的,當(dāng)你不停的把磁盤上的數(shù)據(jù)頁(yè)加載到空閑緩存頁(yè)里去,free 鏈表中不停的移除空閑緩存頁(yè),遲早有那么一瞬間,free 鏈表中已經(jīng)沒有空閑緩存頁(yè),這時(shí)候就需要淘汰掉一些緩存頁(yè),那淘汰誰(shuí)呢?這就需要利用緩存命中率了,緩存命中多的就是常用的,那不常用的就可以淘汰了。所以引入 LRU 鏈表來判斷哪些緩存頁(yè)是不常用的。

那 LRU 鏈表的淘汰策略是什么樣的呢?

假設(shè)我們從磁盤加載一個(gè)數(shù)據(jù)頁(yè)到緩存頁(yè)的時(shí)候,就把這個(gè)緩存頁(yè)的描述數(shù)據(jù)塊放到 LRU 鏈表頭部去,那么只要有數(shù)據(jù)的緩存頁(yè),他都會(huì)在 LRU 里了,而且最近被加載數(shù)據(jù)的緩存頁(yè),都會(huì)放到 LRU 鏈表的頭部去,然后加入某個(gè)緩存頁(yè)在尾部,只要發(fā)生查詢,就把它移到頭部,那么最后尾部就是需要淘汰了。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

但是這樣真的就可以嗎?

第一種情況預(yù)讀機(jī)制破壞

由于預(yù)讀機(jī)制會(huì)把相鄰的沒有被訪問到的數(shù)據(jù)頁(yè)加載到緩存里,實(shí)際上只有一個(gè)緩存頁(yè)是被訪問了,另外一個(gè)通過預(yù)讀機(jī)制加載的緩存頁(yè),其實(shí)并沒有人訪問,此時(shí)這兩個(gè)緩存頁(yè)可都在 LRU 鏈表的前面,如下圖

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

這個(gè)時(shí)候,假如沒有空閑緩存頁(yè)了,那么此時(shí)要加載新的數(shù)據(jù)頁(yè)了,是不是就要從 LRU 鏈表的尾部把所謂的“最近最少使用的一個(gè)緩存頁(yè)”給拿出來,刷入磁盤,然后騰出來一個(gè)空閑緩存頁(yè)了。這樣顯然是很不合理的。

第二種情況可能導(dǎo)致頻繁被訪問的緩存頁(yè)被淘汰的場(chǎng)景

全表掃描導(dǎo)致他直接一下子把這個(gè)表里所有的數(shù)據(jù)頁(yè),都從磁盤加載到 Buffer Pool 里去。這個(gè)時(shí)候可能會(huì)一下子就把這個(gè)表的所有數(shù)據(jù)頁(yè)都一一裝入各個(gè)緩存頁(yè)里去!此時(shí)可能 LRU 鏈表中排在前面的一大串緩存頁(yè),都是全表掃描加載進(jìn)來的緩存頁(yè)!那么如果這次全表掃描過后,后續(xù)幾乎沒用到這個(gè)表里的數(shù)據(jù)呢?此時(shí) LRU 鏈表的尾部,可能全部都是之前一直被頻繁訪問的那些緩存頁(yè)!然后當(dāng)你要淘汰掉一些緩存頁(yè)騰出空間的時(shí)候,就會(huì)把 LRU 鏈表尾部一直被頻繁訪問的緩存頁(yè)給淘汰掉了,而留下了之前全表掃描加載進(jìn)來的大量的不經(jīng)常訪問的緩存頁(yè)!

優(yōu)化 LRU 算法:基于冷熱數(shù)據(jù)分離的思想設(shè)計(jì) LRU 鏈表

MySQL 在設(shè)計(jì) LRU 鏈表的時(shí)候,采取的實(shí)際上是冷熱數(shù)據(jù)分離的思想。LRU 鏈表,會(huì)被拆分為兩個(gè)部分,一部分是熱數(shù)據(jù),一部分是冷數(shù)據(jù),這個(gè)冷熱數(shù)據(jù)的比例是由 innodb_old_blocks_pct 參數(shù)控制的,他默認(rèn)是 37,也就是說冷數(shù)據(jù)占比 37%。數(shù)據(jù)頁(yè)第一次被加載到緩存的時(shí)候,實(shí)際上緩存頁(yè)會(huì)被放在冷數(shù)據(jù)區(qū)域的鏈表頭部。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

然后 MySQL 設(shè)定了一個(gè)規(guī)則,他設(shè)計(jì)了一個(gè) innodb_old_blocks_time 參數(shù),默認(rèn)值 1000,也就是 1000 毫秒也就是說,必須是一個(gè)數(shù)據(jù)頁(yè)被加載到緩存頁(yè)之后,在 1s 之后,你訪問這個(gè)緩存頁(yè),它會(huì)被挪動(dòng)到熱數(shù)據(jù)區(qū)域的鏈表頭部去。因?yàn)榧僭O(shè)你加載了一個(gè)數(shù)據(jù)頁(yè)到緩存去,然后過了 1s 之后你還訪問了這個(gè)緩存頁(yè),說明你后續(xù)很可能會(huì)經(jīng)常要訪問它,這個(gè)時(shí)間限制就是 1s,因此只有 1s 后你訪問了這個(gè)緩存頁(yè),他才會(huì)給你把緩存頁(yè)放到熱數(shù)據(jù)區(qū)域的鏈表頭部去。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

這樣的話預(yù)讀和全表掃描的數(shù)據(jù)都只會(huì)在冷數(shù)據(jù)頭部,不會(huì)一開始就進(jìn)去熱數(shù)據(jù)區(qū)。

LRU 算法極致優(yōu)化

LRU 鏈表的熱數(shù)據(jù)區(qū)域的訪問規(guī)則優(yōu)化一下,即只有在熱數(shù)據(jù)區(qū)域的后 3 / 4 部分的緩存頁(yè)被訪問了,才會(huì)給你移動(dòng)到鏈表頭部去。如果你是熱數(shù)據(jù)區(qū)域的前面 1 / 4 的緩存頁(yè)被訪問,他是不會(huì)移動(dòng)到鏈表頭部去的。

舉個(gè)例子,假設(shè)熱數(shù)據(jù)區(qū)域的鏈表里有 100 個(gè)緩存頁(yè),那么排在前面的 25 個(gè)緩存頁(yè),他即使被訪問了,也不會(huì)移動(dòng)到鏈表頭部去的。但是對(duì)于排在后面的 75 個(gè)緩存頁(yè),他只要被訪問,就會(huì)移動(dòng)到鏈表頭部去。這樣的話,他就可以盡可能的減少鏈表中的節(jié)點(diǎn)移動(dòng)了。

LRU 鏈表淘汰緩存頁(yè)時(shí)機(jī)

MySQL 在執(zhí)行 CRUD 的時(shí)候,首先就是大量的操作緩存頁(yè)以及對(duì)應(yīng)的幾個(gè)鏈表。然后在緩存頁(yè)都滿的時(shí)候,必然要想辦法把一些緩存頁(yè)給刷入磁盤,然后清空這幾個(gè)緩存頁(yè),接著把需要的數(shù)據(jù)頁(yè)加載到緩存頁(yè)里去!

我們已經(jīng)知道,他是根據(jù) LRU 鏈表去淘汰緩存頁(yè)的,那么他到底是什么時(shí)候把 LRU 鏈表的冷數(shù)據(jù)區(qū)域中的緩存頁(yè)刷入磁盤的呢?實(shí)際上他有以下三個(gè)時(shí)機(jī):

定時(shí)把 LRU 尾部的部分緩存頁(yè)刷入磁盤

后臺(tái)線程,運(yùn)行一個(gè)定時(shí)任務(wù),這個(gè)定時(shí)任務(wù)每隔一段時(shí)間就會(huì)把 LRU 鏈表的冷數(shù)據(jù)區(qū)域的尾部的一些緩存頁(yè),刷入磁盤里去,清空這幾個(gè)緩存頁(yè),把他們加入回 free 鏈表去。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

把 flush 鏈表中的一些緩存頁(yè)定時(shí)刷入磁盤

如果只是把 LRU 鏈表的冷數(shù)據(jù)區(qū)域的緩存頁(yè)刷入磁盤是不夠,因?yàn)殒湵淼臒釘?shù)據(jù)區(qū)域里的很多緩存頁(yè)可能也會(huì)被頻繁的修改,難道他們永遠(yuǎn)都不刷入磁盤中了嗎?

所以這個(gè)后臺(tái)線程同時(shí)也會(huì)在 MySQL 不怎么繁忙的時(shí)候,把 flush 鏈表中的緩存頁(yè)都刷入磁盤中,這樣被你修改過的數(shù)據(jù),遲早都會(huì)刷入磁盤的!

只要 flush 鏈表中的一波緩存頁(yè)被刷入了磁盤,那么這些緩存頁(yè)也會(huì)從 flush 鏈表和 lru 鏈表中移除,然后加入到 free 鏈表中去!

所以整體效果就是不停的加載數(shù)據(jù)到緩存頁(yè)里去,不停的查詢和修改緩存數(shù)據(jù),然后 free 鏈表中的緩存頁(yè)不停的在減少,flush 鏈表中的緩存頁(yè)不停的在增加,lru 鏈表中的緩存頁(yè)不停的在增加和移動(dòng)。

另外一邊,你的后臺(tái)線程不停的在把 lru 鏈表的冷數(shù)據(jù)區(qū)域的緩存頁(yè)以及 flush 鏈表的緩存頁(yè),刷入磁盤中來清空緩存頁(yè),然后 flush 鏈表和 lru 鏈表中的緩存頁(yè)在減少,free 鏈表中的緩存頁(yè)在增加。

free 鏈表沒有空閑緩存頁(yè)

如果所有的 free 鏈表都被使用了,這個(gè)時(shí)候如果要從磁盤加載數(shù)據(jù)頁(yè)到一個(gè)空閑緩存頁(yè)中,此時(shí)就會(huì)從 LRU 鏈表的冷數(shù)據(jù)區(qū)域的尾部找到一個(gè)緩存頁(yè),他一定是最不經(jīng)常使用的緩存頁(yè)!然后把他刷入磁盤和清空,然后把數(shù)據(jù)頁(yè)加載到這個(gè)騰出來的空閑緩存頁(yè)里去!

總結(jié)一下,三個(gè)鏈表的使用情況,Buffer Pool 被使用的時(shí)候,實(shí)際上會(huì)頻繁的從磁盤上加載數(shù)據(jù)頁(yè)到他的緩存頁(yè)里去,然后 free 鏈表、flush 鏈表、lru 鏈表都會(huì)同時(shí)被使用,比如數(shù)據(jù)加載到一個(gè)緩存頁(yè),free 鏈表里會(huì)移除這個(gè)緩存頁(yè),然后 lru 鏈表的冷數(shù)據(jù)區(qū)域的頭部會(huì)放入這個(gè)緩存頁(yè)。

然后如果你要是修改了一個(gè)緩存頁(yè),那么 flush 鏈表中會(huì)記錄這個(gè)臟頁(yè),lru 鏈表中還可能會(huì)把你從冷數(shù)據(jù)區(qū)域移動(dòng)到熱數(shù)據(jù)區(qū)域的頭部去。

如果你是查詢了一個(gè)緩存頁(yè),那么此時(shí)就會(huì)把這個(gè)緩存頁(yè)在 lru 鏈表中移動(dòng)到熱數(shù)據(jù)區(qū)域去,或者在熱數(shù)據(jù)區(qū)域中也有可能會(huì)移動(dòng)到頭部去。

Redo log Buffer 重做日志緩沖

InnoDB 有 buffer pool(簡(jiǎn)稱 bp)。bp 是數(shù)據(jù)庫(kù)頁(yè)面的緩存,對(duì) InnoDB 的任何修改操作都會(huì)首先在 bp 的 page 上進(jìn)行,然后這樣的頁(yè)面將被標(biāo)記為 dirty(臟頁(yè)) 并被放到專門的 flush list 上,后續(xù)將由 master thread 或?qū)iT的刷臟線程階段性的將這些頁(yè)面寫入磁盤(disk or ssd)。

這樣的好處是避免每次寫操作都操作磁盤導(dǎo)致大量的隨機(jī) IO,階段性的刷臟可以將多次對(duì)頁(yè)面的修改 merge 成一次 IO 操作,同時(shí)異步寫入也降低了訪問的時(shí)延。然而,如果在 dirty page 還未刷入磁盤時(shí),server 非正常關(guān)閉,這些修改操作將會(huì)丟失,如果寫入操作正在進(jìn)行,甚至?xí)捎趽p壞數(shù)據(jù)文件導(dǎo)致數(shù)據(jù)庫(kù)不可用。

為了避免上述問題的發(fā)生,Innodb 將所有對(duì)頁(yè)面的修改操作寫入一個(gè)專門的文件,并在數(shù)據(jù)庫(kù)啟動(dòng)時(shí)從此文件進(jìn)行恢復(fù)操作,這個(gè)文件就是 redo log file。這樣的技術(shù)推遲了 bp 頁(yè)面的刷新,從而提升了數(shù)據(jù)庫(kù)的吞吐,有效的降低了訪問時(shí)延。

帶來的問題是額外的寫 redo log 操作的開銷(順序 IO,當(dāng)然很快),以及數(shù)據(jù)庫(kù)啟動(dòng)時(shí)恢復(fù)操作所需的時(shí)間。

redo 日志由兩部分構(gòu)成:redo log buffer、redo log file(在磁盤文件那部分介紹)。innodb 是支持事務(wù)的存儲(chǔ)引擎,在事務(wù)提交時(shí),必須先將該事務(wù)的所有日志寫入到 redo 日志文件中,待事務(wù)的 commit 操作完成才算整個(gè)事務(wù)操作完成。在每次將 redo log buffer 寫入 redo log file 后,都需要調(diào)用一次 fsync 操作,因?yàn)橹刈鋈罩揪彌_只是把內(nèi)容先寫入操作系統(tǒng)的緩沖系統(tǒng)中,并沒有確保直接寫入到磁盤上,所以必須進(jìn)行一次 fsync 操作。因此,磁盤的性能在一定程度上也決定了事務(wù)提交的性能(具體后面 redo log 落盤機(jī)制介紹)。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

InnoDB 存儲(chǔ)引擎會(huì)首先將重做日志信息先放入重做日志緩沖中,然后在按照一定頻率將其刷新到重做日志文件,重做日志緩沖一般不需要設(shè)置的很大,因?yàn)橐话闱闆r每一秒鐘都會(huì)將重做日志緩沖刷新到日志文件中,可通過配置參數(shù) Innodb_log_buffer_size 控制,默認(rèn)為 8MB。

Double Write 雙寫

如果說 Insert Buffer 給 InnoDB 存儲(chǔ)引擎帶來了性能上的提升,那么 Double wtite 帶給 InnoDB 存儲(chǔ)引擎的是數(shù)據(jù)頁(yè)的可靠性。

InnoDB 的 Page Size 一般是 16KB,其數(shù)據(jù)校驗(yàn)也是針對(duì)這 16KB 來計(jì)算的,將數(shù)據(jù)寫入到磁盤是以 Page 為單位進(jìn)行操作的。我們知道,由于文件系統(tǒng)對(duì)一次大數(shù)據(jù)頁(yè)(例如 InnoDB 的 16KB)大多數(shù)情況下不是原子操作,這意味著如果服務(wù)器宕機(jī)了,可能只做了部分寫入。16K 的數(shù)據(jù),寫入 4K 時(shí),發(fā)生了系統(tǒng)斷電 os crash,只有一部分寫是成功的,這種情況下就是 partial page write 問題。

有經(jīng)驗(yàn)的 DBA 可能會(huì)想到,如果發(fā)生寫失效,MySQL 可以根據(jù) redo log 進(jìn)行恢復(fù)。這是一個(gè)辦法,但是必須清楚地認(rèn)識(shí)到,redo log 中記錄的是對(duì)頁(yè)的物理修改,如偏移量 800,寫’aaaa’記錄。如果這個(gè)頁(yè)本身已經(jīng)發(fā)生了損壞,再對(duì)其進(jìn)行重做是沒有意義的。MySQL 在恢復(fù)的過程中檢查 page 的 checksum,checksum 就是檢查 page 的最后事務(wù)號(hào),發(fā)生 partial page write 問題時(shí),page 已經(jīng)損壞,找不到該 page 中的事務(wù)號(hào)。在 InnoDB 看來,這樣的數(shù)據(jù)頁(yè)是無法通過 checksum 驗(yàn)證的,就無法恢復(fù)。即時(shí)我們強(qiáng)制讓其通過驗(yàn)證,也無法從崩潰中恢復(fù),因?yàn)楫?dāng)前 InnoDB 存在的一些日志類型,有些是邏輯操作,并不能做到冪等。

為了解決這個(gè)問題,InnoDB 實(shí)現(xiàn)了 double write buffer,簡(jiǎn)單來說,就是在寫數(shù)據(jù)頁(yè)之前,先把這個(gè)數(shù)據(jù)頁(yè)寫到一塊獨(dú)立的物理文件位置(ibdata),然后再寫到數(shù)據(jù)頁(yè)。這樣在宕機(jī)重啟時(shí),如果出現(xiàn)數(shù)據(jù)頁(yè)損壞,那么在應(yīng)用 redo log 之前,需要通過該頁(yè)的副本來還原該頁(yè),然后再進(jìn)行 redo log 重做,這就是 double write。double write 技術(shù)帶給 innodb 存儲(chǔ)引擎的是數(shù)據(jù)頁(yè)的可靠性,下面對(duì) doublewrite 技術(shù)進(jìn)行解析

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

如上圖所示,Double Write 由兩部分組成,一部分是內(nèi)存中的 double write buffer,大小為 2MB,另一部分是物理磁盤上共享表空間連續(xù)的 128 個(gè)頁(yè),大小也為 2MB。在對(duì)緩沖池的臟頁(yè)進(jìn)行刷新時(shí),并不直接寫磁盤,而是通過 memcpy 函數(shù)將臟頁(yè)先復(fù)制到內(nèi)存中的該區(qū)域,之后通過 double write buffer 再分兩次,每次 1MB 順序地寫入共享表空間的物理磁盤上,然后馬上調(diào)用 fsync 函數(shù),同步磁盤,避免操作系統(tǒng)緩沖寫帶來的問題。在完成 double write 頁(yè)的寫入后,再將 double wirite buffer 中的頁(yè)寫入各個(gè)表空間文件中。

在這個(gè)過程中,doublewrite 是順序?qū)懀_銷并不大,在完成 doublewrite 寫入后,在將 double write buffer 寫入各表空間文件,這時(shí)是離散寫入。

如果操作系統(tǒng)在將頁(yè)寫入磁盤的過程中發(fā)生了崩潰,在恢復(fù)過程中,InnoDB 存儲(chǔ)引擎可以從共享表空間中的 double write 中找到該頁(yè)的一個(gè)副本,將其復(fù)制到表空間文件中,再應(yīng)用重做日志。

InnoDB 存儲(chǔ)引擎第二部分:后臺(tái)線程

IO 線程

在 InnoDB 中使用了大量的 AIO(Async IO)來做讀寫處理,這樣可以極大提高數(shù)據(jù)庫(kù)的性能。在 InnoDB 1.0 版本之前共有 4 個(gè) IO Thread,分別是 write,read,insert buffer 和 log thread,后來版本將 read thread 和 write thread 分別增大到了 4 個(gè),一共有 10 個(gè)了。

– read thread:負(fù)責(zé)讀取操作,將數(shù)據(jù)從磁盤加載到緩存 page 頁(yè)。4 個(gè)

– write thread:負(fù)責(zé)寫操作,將緩存臟頁(yè)刷新到磁盤。4 個(gè)

– log thread:負(fù)責(zé)將日志緩沖區(qū)內(nèi)容刷新到磁盤。1 個(gè)

– insert buffer thread:負(fù)責(zé)將寫緩沖內(nèi)容刷新到磁盤。1 個(gè)

Purge 線程

事務(wù)提交之后,其使用的 undo 日志將不再需要,因此需要 Purge Thread 回收已經(jīng)分配的 undo 頁(yè)。show variables like %innodb*purge*threads%

Page Cleaner 線程

作用是將臟數(shù)據(jù)刷新到磁盤,臟數(shù)據(jù)刷盤后相應(yīng)的 redo log 也就可以覆蓋,即可以同步數(shù)據(jù),又能達(dá)到 redo log 循環(huán)使用的目的。會(huì)調(diào)用 write thread 線程處理。show variables like %innodb*page*cleaners%

InnoDB 存儲(chǔ)引擎第三部分:磁盤文件

InnoDB 的主要的磁盤文件主要分為三大塊:一是系統(tǒng)表空間,二是用戶表空間,三是 redo 日志文件和歸檔文件。

二進(jìn)制文件(binlong)等文件是 MySQL Server 層維護(hù)的文件,所以未列入 InnoDB 的磁盤文件中。

系統(tǒng)表空間和用戶表空間

系統(tǒng)表空間包含 InnoDB 數(shù)據(jù)字典(元數(shù)據(jù)以及相關(guān)對(duì)象)并且 double write buffer , change buffer , undo logs 的存儲(chǔ)區(qū)域。

系統(tǒng)表空間也默認(rèn)包含任何用戶在系統(tǒng)表空間創(chuàng)建的表數(shù)據(jù)和索引數(shù)據(jù)。

系統(tǒng)表空間是一個(gè)共享的表空間,因?yàn)樗潜欢鄠€(gè)表共享的。

系統(tǒng)表空間是由一個(gè)或者多個(gè)數(shù)據(jù)文件組成。默認(rèn)情況下,1 個(gè)初始大小為 10MB,名為 ibdata1 的系統(tǒng)數(shù)據(jù)文件在 MySQL 的 data 目錄下被創(chuàng)建。用戶可以使用 innodb_data_file_path 對(duì)數(shù)據(jù)文件的大小和數(shù)量進(jìn)行配置。

innodb_data_file_path 的格式如下:

innodb_data_file_path=datafile1[,datafile2]...

用戶可以通過多個(gè)文件組成一個(gè)表空間,同時(shí)制定文件的屬性:

innodb_data_file_path = /db/ibdata1:1000M;/dr2/db/ibdata2:1000M:autoextend

這里將 /db/ibdata1 和 /dr2/db/ibdata2 兩個(gè)文件組成系統(tǒng)表空間。如果這兩個(gè)文件位于不同的磁盤上,磁盤的負(fù)載可能被平均,因此可以提高數(shù)據(jù)庫(kù)的整體性能。兩個(gè)文件的文件名之后都跟了屬性,表示文件 ibdata1 的大小為 1000MB,文件 ibdata2 的大小為 1000MB,而且用完空間之后可以自動(dòng)增長(zhǎng)。

設(shè)置 innodb_data_file_path 參數(shù)之后,所有基于 InnoDB 存儲(chǔ)引擎的表的數(shù)據(jù)都會(huì)記錄到該系統(tǒng)表空間中,如果設(shè)置了參數(shù) innodb_file_per_table,則用戶可以將每個(gè)基于 InnoDB 存儲(chǔ)引擎的表產(chǎn)生一個(gè)獨(dú)立的用戶空間。

用戶表空間的命名規(guī)則為:表名.ibd。通過這種方式,用戶不用將所有數(shù)據(jù)都存放于默認(rèn)的系統(tǒng)表空間中,但是用戶表空間只存儲(chǔ)該表的數(shù)據(jù)、索引和插入緩沖 BITMAP 等信息,其余信息還是存放在默認(rèn)的系統(tǒng)表空間中。

下圖顯示 InnoDB 存儲(chǔ)引擎對(duì)于文件的存儲(chǔ)方式,其中 frm 文件是表結(jié)構(gòu)定義文件,記錄每個(gè)表的表結(jié)構(gòu)定義。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

重做日志文件(redo log file)和歸檔文件

默認(rèn)情況下,在 InnoDB 存儲(chǔ)引擎的數(shù)據(jù)目錄下會(huì)有兩個(gè)名為 ib_logfile0 和 ib_logfile1 的文件,這就是 InnoDB 的重做文件(redo log file),它記錄了對(duì)于 InnoDB 存儲(chǔ)引擎的事務(wù)日志。

當(dāng) InnoDB 的數(shù)據(jù)存儲(chǔ)文件發(fā)生錯(cuò)誤時(shí),重做日志文件就能派上用場(chǎng)。InnoDB 存儲(chǔ)引擎可以使用重做日志文件將數(shù)據(jù)恢復(fù)為正確狀態(tài),以此來保證數(shù)據(jù)的正確性和完整性。

每個(gè) InnoDB 存儲(chǔ)引擎至少有 1 個(gè)重做日志文件,每個(gè)文件組下至少有 2 個(gè)重做日志文件,加默認(rèn)的 ib_logfile0 和 ib_logfile1。

為了得到更高的可靠性,用戶可以設(shè)置多個(gè)鏡像日志組,將不同的文件組放在不同的磁盤上,以此來提高重做日志的高可用性。

在日志組中每個(gè)重做日志文件的大小一致,并以【循環(huán)寫入】的方式運(yùn)行。InnoDB 存儲(chǔ)引擎先寫入重做日志文件 1,當(dāng)文件被寫滿時(shí),會(huì)切換到重做日志文件 2,再當(dāng)重做日志文件 2 也被寫滿時(shí),再切換到重做日志 1。

用戶可以使用 Innodb_log_file_size 來設(shè)置重做日志文件的大小,這對(duì) InnoDB 存儲(chǔ)引擎的性能有著非常大的影響。

如果重做日志文件設(shè)置的太大,數(shù)據(jù)丟失時(shí),恢復(fù)時(shí)可能需要很長(zhǎng)的時(shí)間;另一個(gè)方面,如果設(shè)置的太小,重做日志文件太小會(huì)導(dǎo)致依據(jù) checkpoint 的檢查需要頻繁刷新臟頁(yè)到磁盤中,導(dǎo)致性能的抖動(dòng)。

重做日志的落盤機(jī)制

InnoDB 對(duì)于數(shù)據(jù)文件和日志文件的刷盤遵守 WAL(write ahead redo log)和 Force-log-at-commit 兩種規(guī)則,二者保證了事務(wù)的持久性。WAL 要求數(shù)據(jù)的變更寫入到磁盤前,首先必須將內(nèi)存中的日志寫入到磁盤;Force-log-at-commit 要求當(dāng)一個(gè)事務(wù)提交時(shí),所有產(chǎn)生的日志都必須刷新到磁盤上,如果日志刷新成功后,緩沖池中的數(shù)據(jù)刷新到磁盤前數(shù)據(jù)庫(kù)發(fā)生了宕機(jī),那么重啟時(shí),數(shù)據(jù)庫(kù)可以從日志中恢復(fù)數(shù)據(jù)。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

如上圖所示,InnoDB 在緩沖池中變更數(shù)據(jù)時(shí),會(huì)首先將相關(guān)變更寫入重做日志緩沖中,然后再按時(shí)(比如每秒刷新機(jī)制)或者當(dāng)事務(wù)提交時(shí)寫入磁盤,這符合 Force-log-at-commit 原則;當(dāng)重做日志寫入磁盤后,緩沖池中的變更數(shù)據(jù)才會(huì)依據(jù) checkpoint 機(jī)制寫入到磁盤中,這符合 WAL 原則。

在 checkpoint 擇時(shí)機(jī)制中,就有重做日志文件寫滿的判斷,所以,如前文所述,如果重做日志文件太小,經(jīng)常被寫滿,就會(huì)頻繁導(dǎo)致 checkpoint 將更改的數(shù)據(jù)寫入磁盤,導(dǎo)致性能抖動(dòng)。

操作系統(tǒng)的文件系統(tǒng)是帶有緩存的,當(dāng) InnoDB 向磁盤寫入數(shù)據(jù)時(shí),有可能只是寫入到了文件系統(tǒng)的緩存中,沒有真正的“落袋為安”。

InnoDB 的 innodb_flush_log_at_trx_commit 屬性可以控制每次事務(wù)提交時(shí) InnoDB 的行為。當(dāng)屬性值為 0 時(shí),事務(wù)提交時(shí),不會(huì)對(duì)重做日志進(jìn)行寫入操作,而是等待主線程按時(shí)寫入;當(dāng)屬性值為 1 時(shí),事務(wù)提交時(shí),會(huì)將重做日志寫入文件系統(tǒng)緩存,并且調(diào)用文件系統(tǒng)的 fsync,將文件系統(tǒng)緩沖中的數(shù)據(jù)真正寫入磁盤存儲(chǔ),確保不會(huì)出現(xiàn)數(shù)據(jù)丟失;當(dāng)屬性值為 2 時(shí),事務(wù)提交時(shí),也會(huì)將日志文件寫入文件系統(tǒng)緩存,但是不會(huì)調(diào)用 fsync,而是讓文件系統(tǒng)自己去判斷何時(shí)將緩存寫入磁盤。

日志的刷盤機(jī)制如下圖所示:

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

Innodb_flush_log_at_commit 是 InnoDB 性能調(diào)優(yōu)的一個(gè)基礎(chǔ)參數(shù),涉及 InnoDB 的寫入效率和數(shù)據(jù)安全。當(dāng)參數(shù)數(shù)值為 0 時(shí),寫入效率最高,但是數(shù)據(jù)安全最低;參數(shù)值為 1 時(shí),寫入效率最低,但是數(shù)據(jù)安全最高;參數(shù)值為 2 時(shí),二者都是中等水平,一般建議將屬性值設(shè)置為 1,以獲得較高的安全性,而且也只有設(shè)置為 1,才能保證事務(wù)的持久性。

用一條 UPDATE 語(yǔ)句再來了解 InnoDB 存儲(chǔ)引擎

有了上面 InnoDB 存儲(chǔ)引擎的架構(gòu)基礎(chǔ)介紹,我們?cè)賮矸治鲆幌乱淮?UPDATE 數(shù)據(jù)更新具體流程。

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

我們把這張圖分為上下兩部分來看,上面那部分是 MySQL Server 層處理流程,下面那部分是 MySQL InnoDB 存儲(chǔ)引擎處理流程。

MySQL Server 層處理流程

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

這部分處理流程無關(guān)于哪個(gè)存儲(chǔ)引擎,它是 Server 層處理的,具體步驟如下:

用戶各種操作觸發(fā)后臺(tái) sql 執(zhí)行,通過 web 項(xiàng)目中自帶的數(shù)據(jù)庫(kù)連接池:如 dbcp、c3p0、druid 等,與數(shù)據(jù)庫(kù)服務(wù)器的數(shù)據(jù)庫(kù)連接池建立網(wǎng)絡(luò)連接;

數(shù)據(jù)庫(kù)連接池中的線程監(jiān)聽到請(qǐng)求后,將接收到的 sql 語(yǔ)句通過 SQL 接口響應(yīng)給查詢解析器,查詢解析器將 sql 按照 sql 的語(yǔ)法解析出查詢哪個(gè)表的哪些字段,查詢條件是啥;

再通過查詢優(yōu)化器處理,選擇該 sq 最優(yōu)的一套執(zhí)行計(jì)劃;

然后執(zhí)行器負(fù)責(zé)調(diào)用存儲(chǔ)引擎的一系列接口,執(zhí)行該計(jì)劃而完成整個(gè) sql 語(yǔ)句的執(zhí)行

這部分流程和上面分析的 一次 Select 請(qǐng)求處理流程分析的基本一致。

InnoDB 存儲(chǔ)引擎處理流程

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

具體執(zhí)?語(yǔ)句得要存儲(chǔ)引擎來完成,如上圖所示:

更新 users 表中 id=10 的這條數(shù)據(jù),如果緩沖池中沒有該條數(shù)據(jù)的,得要先從磁盤中將被更新數(shù)據(jù)的原始數(shù)據(jù)加載到緩沖池中。

同時(shí)為了保證并發(fā)更新數(shù)據(jù)安全問題,會(huì)對(duì)這條數(shù)據(jù)先加鎖,防?其他事務(wù)進(jìn)?更新。

接著將更新前的值先備份寫?到 undo log 中(便于事務(wù)回滾時(shí)取舊數(shù)據(jù)),?如 update 語(yǔ)句即存儲(chǔ)被更新字段之前的值。

更新 buffer pool 中的緩存數(shù)據(jù)為最新的數(shù)據(jù),那么此時(shí)內(nèi)存中的數(shù)據(jù)為臟數(shù)據(jù)(內(nèi)存中數(shù)據(jù)和磁盤中數(shù)據(jù)不一致)

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

?此就完成了在緩沖池中的執(zhí)?流程(如上圖)。

緩沖池中更新完數(shù)據(jù)后,需要將本次的更新信息順序?qū)懙?Redo Log ?志,因?yàn)楝F(xiàn)在已經(jīng)把內(nèi)存里的數(shù)據(jù)進(jìn)行了修改,但是磁盤上的數(shù)據(jù)還沒修改,此時(shí)萬(wàn)一 MySQL 所在的機(jī)器宕機(jī)了,必然會(huì)導(dǎo)致內(nèi)存里修改過的數(shù)據(jù)丟失,redo 日志就是記錄下來你對(duì)數(shù)據(jù)做了什么修改,比如對(duì)“id=10 這行記錄修改了 name 字段的值為 xxx”,這就是一個(gè)日志,用來在 MySQL 突然宕機(jī)的時(shí)候,用來恢復(fù)你更新過的數(shù)據(jù)的。不過注意的是此時(shí) Redo Log 還沒有落盤到日志文件。

這個(gè)時(shí)候思考一個(gè)問題:如果還沒提交事務(wù),MySQL 宕機(jī)了怎么辦?

上面我們知道到目前我們修改了內(nèi)存數(shù)據(jù),然后記錄了 Redo Log Buffer 日志緩沖,如果這個(gè)時(shí)候 MySQL 奔潰,內(nèi)存數(shù)據(jù)和  Redo Log Buffer 數(shù)據(jù)都會(huì)丟失,但是此時(shí)數(shù)據(jù)丟失并不要緊,因?yàn)橐粭l更新語(yǔ)句,沒提交事務(wù),就代表他沒執(zhí)行成功,此時(shí) MySQL 宕機(jī)雖然導(dǎo)致內(nèi)存里的數(shù)據(jù)都丟失了,但是你會(huì)發(fā)現(xiàn),磁盤上的數(shù)據(jù)依然還停留在原樣子。

接下來要提交事物了,此時(shí)就會(huì)根據(jù)一定的策略把 redo 日志從 redo log buffer 里刷入到磁盤文件里去,此時(shí)這個(gè)策略是通過 innodb_flush_log_at_trx_commit 來配置的。

innodb_flush_log_at_trx_commit=0,表示提交事物不會(huì)把 redo log buffer 里的數(shù)據(jù)刷入磁盤文件的,此時(shí)可能你都提交事務(wù)了,結(jié)果 mysql 宕機(jī)了,然后此時(shí)內(nèi)存里的數(shù)據(jù)全部丟失,所以這種方式不可取。

innodb_flush_log_at_trx_commit=1,redo log 從內(nèi)存刷入到磁盤文件里去,只要事務(wù)提交成功,那么 redo log 就必然在磁盤里了,所以如果這個(gè)時(shí)候 MySQL 奔潰,可以根據(jù) Redo Log 日志恢復(fù)數(shù)據(jù)。

innodb_flush_log_at_trx_commit=2,提交事務(wù)的時(shí)候,把 redo 日志寫入磁盤文件對(duì)應(yīng)的 os cache 緩存里去,而不是直接進(jìn)入磁盤文件,可能 1 秒后才會(huì)把 os cache 里的數(shù)據(jù)寫入到磁盤文件里去。

提交事務(wù)的時(shí)候,同時(shí)會(huì)寫入 binlog,binlog 也有不同的刷盤策略,有一個(gè) sync_binlog 參數(shù)可以控制 binlog 的刷盤策略,他的默認(rèn)值是 0,此時(shí)你把 binlog 寫入磁盤的時(shí)候,其實(shí)不是直接進(jìn)入磁盤文件,而是進(jìn)入 os cache 內(nèi)存緩存。?般我們?yōu)榱吮WC數(shù)據(jù)不丟失會(huì)配置雙 1 策略,Redo Log 和 Binlog 落盤策略都選擇 1。

Binlog 落盤后,再將 Binlog 的?件名、?件所在路徑信息以及 commit 標(biāo)記給同步順序?qū)懙?Redo log 中,這一步的意義是用來保持 redo log 日志與 binlog 日志一致的。commit 標(biāo)記是判定事務(wù)是否成功提交的?個(gè)?較重要的標(biāo)準(zhǔn),舉個(gè)例子,如果如果第 5 步或者第 6 步執(zhí)行成功后 MySQL 就奔潰了,這個(gè)時(shí)候因?yàn)闆]有最終的事務(wù) commit 標(biāo)記在 redo 日志里,所以此次事務(wù)可以判定為不成功。不會(huì)說 redo 日志文件里有這次更新的日志,但是 binlog 日志文件里沒有這次更新的日志,不會(huì)出現(xiàn)數(shù)據(jù)不一致的問題。

做完前面后,內(nèi)存數(shù)據(jù)已經(jīng)修改,事物已經(jīng)提交,日志已經(jīng)落盤,但是磁盤數(shù)據(jù)還沒有同步修改。InnoDB 存儲(chǔ)引擎后臺(tái)有?個(gè) IO 線程,會(huì)在數(shù)據(jù)庫(kù)壓?的低峰期間,將緩沖池中被事務(wù)更新、但還沒來得及寫到磁盤中的數(shù)據(jù)(臟數(shù)據(jù),因?yàn)榇疟P數(shù)據(jù)和內(nèi)存數(shù)據(jù)已經(jīng)不?致了)給刷到磁盤中,完成事務(wù)的持久化。

所以 InnoDB 處理寫入過程可以用下面這幅圖表示

mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些

到此,關(guān)于“mysql 體系結(jié)構(gòu)和 InnoDB 存儲(chǔ)引擎知識(shí)有哪些”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注丸趣 TV 網(wǎng)站,丸趣 TV 小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

正文完
 
丸趣
版權(quán)聲明:本站原創(chuàng)文章,由 丸趣 2023-07-15發(fā)表,共計(jì)17485字。
轉(zhuǎn)載說明:除特殊說明外本站除技術(shù)相關(guān)以外文章皆由網(wǎng)絡(luò)搜集發(fā)布,轉(zhuǎn)載請(qǐng)注明出處。
評(píng)論(沒有評(píng)論)
主站蜘蛛池模板: 交城县| 霍邱县| 康定县| 秀山| 景宁| 韩城市| 德江县| 呈贡县| 仁寿县| 松潘县| 来安县| 吐鲁番市| 望城县| 余江县| 错那县| 射阳县| 唐河县| 阳原县| 许昌县| 正宁县| 呼伦贝尔市| 绥棱县| 清流县| 博爱县| 永修县| 洪江市| 安龙县| 贵德县| 万山特区| 攀枝花市| 佛山市| 枝江市| 克山县| 库伦旗| 漾濞| 望谟县| 五大连池市| 海阳市| 苗栗县| 米脂县| 平潭县|