您好,登錄后才能下訂單哦!
這篇文章主要介紹“mysql體系結構和InnoDB存儲引擎知識有哪些”,在日常操作中,相信很多人在mysql體系結構和InnoDB存儲引擎知識有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”mysql體系結構和InnoDB存儲引擎知識有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
MySQL基本架構圖
大體來說,MySQL 可以分為 Server 層和存儲引擎層兩部分。
Server 層包括連接器、查詢緩存、分析器、優化器、執行器等,涵蓋 MySQL 的大多數核心服務功能,以及所有的內置函數(如日期、時間、數學和加密函數等),所有跨存儲引擎的功能都在這一層實現,比如存儲過程、觸發器、視圖等。
連接器
連接器就是你連接到數據庫時使用的,負責跟客戶端建立連接、獲取權限、維持和管理連接。
命令: mysql -h$ip -P$port -u$user -p,回車后輸密碼,也可以在 -p 后面輸入密碼,但是有密碼泄露的風險。
show processlist,可以查看連接的情況,Command 列中有一個 Sleep 表示連接空閑。
空閑連接默認8小時會被斷開,可以由wait_timeout參數配置。
在數據庫中,長連接是指連接成功后,如果客戶端持續有請求,則一直使用同一個連接。短連接則是指每次執行完很少的幾次查詢就斷開連接,下次查詢再重新建立一個。
由于建立連接比較耗資源,所以建議盡量使用長連接,但是使用長連接后,MySQL 占用內存漲得特別快,這是因為 MySQL 在執行過程中臨時使用的內存是管理在連接對象里面的。這些資源會在連接斷開的時候才釋放。所以如果長連接累積下來,可能導致內存占用太大,被系統強行殺掉(OOM),從現象看就是 MySQL 異常重啟了。
解決方案:
定期斷開長連接。使用一段時間,或者程序里面判斷執行過一個占用內存的大查詢后,斷開連接,之后要查詢再重連。
如果你用的是 MySQL 5.7 或更新版本,可以在每次執行一個比較大的操作后,通過執行 mysql_reset_connection 來重新初始化連接資源。這個過程不需要重連和重新做權限驗證,但是會將連接恢復到剛剛創建完時的狀態。
查詢緩存
查詢緩存是將之前執行過的語句及其結果以 key-value 對的形式緩存在內存中。key 是查詢的語句,value 是查詢的結果。如果你的查詢能夠直接在這個緩存中找到 key,那么這個 value 就會被直接返回給客戶端。
查詢緩存在MYSQL8時被移除了,由于查詢緩存失效頻繁,命中率低。
分析器
分析器先會做“詞法分析”,識別出里面的字符串分別是什么,代表什么。然后需要做“語法分析”,判斷你輸入的這個 SQL 語句是否滿足 MySQL 語法。
優化器
執行器
存儲引擎層負責數據的存儲和提取。其架構模式是插件式的,支持 InnoDB、MyISAM、Memory 等多個存儲引擎。現在最常用的存儲引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成為了默認存儲引擎。
一條 Select 語句執行流程
上圖以 InnoDB 存儲引擎為例,處理過程如下:
用戶發送請求到 tomcat ,通過 tomcat 鏈接池和 mysql 連接池建立連接,然后通過連接發送 SQL 語句到 MySQL;
MySQL 有一個單獨的監聽線程,讀取到請求數據,得到連接中請求的SQL語句;
將獲取到的SQL數據發送給SQL接口去執行;
SQL接口將SQL發送給SQL解析器進行解析;
將解析好的SQL發送給查詢優化器,找到最優的查詢路勁,然后發給執行器;
執行器根據優化后的執行方案調用存儲引擎的接口按照一定的順序和步驟進行執行。
舉個例子,比如執行器可能會先調用存儲引擎的一個接口,去獲取“users”表中的第一行數據,然后判斷一下這個數據的 “id”字段的值是否等于我們期望的一個值,如果不是的話,那就繼續調用存儲引擎的接口,去獲取“users”表的下一行數據。 就是基于上述的思路,執行器就會去根據我們的優化器生成的一套執行計劃,然后不停的調用存儲引擎的各種接口去完成SQL 語句的執行計劃,大致就是不停的更新或者提取一些數據出來。
在這里涉及到幾個問題:
MySQL驅動到底是什么東西?
以java為例,我們們如果要在Java系統中去訪問一個MySQL數據庫,必須得在系統的依賴中加入一個MySQL驅動,比如在Maven里面要加上
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>
那么這個MySQL驅動到底是個什么東西?其實L驅動就會在底層跟數據庫建立網絡連接,有網絡連接,接著才能去發送請求給數據庫服務器!讓語言編寫的系統通過MySQL驅動去訪問數據庫,如下圖
數據庫連接池到底是用來干什么的?
假設用java開發一個web服務部署在tomcat上,tomcat可以多線程并發處理請求,所以首先一點就是不可能只會創建一個數據庫連接(多個請求去搶一個連接,效率得多低下)。
其次,如果每個請求都去創建一個數據庫連接呢? 這也是非常不好的,因為每次建立一個數據庫連接都很耗時,好不容易建立好了連接,執行完了SQL語句,還把數據庫連接給銷毀,頻繁創建和銷毀帶來性能問題。
所以一般使用數據庫連接池,也就是在一個池子里維持多個數據庫連接,讓多個線程使用里面的不同的數據庫連接去執行SQL語句,然后執行完SQL語句之后,不要銷毀這個數據庫連接,而是把連接放回池子里,后續還可以繼續使用。基于這樣的一個數據庫連接池的機制,就可以解決多個線程并發的使用多個數據庫連接去執行SQL語句的問題,而且還避免了數據庫連接使用完之后就銷毀的問題了。
MySQL數據庫的連接池是用來干什么的?
MySQL數據庫的連接池的作用和java應用端連接池作用一樣,都是起到了復用連接的作用。
InnoDB 存儲引擎
InnoDB 架構簡析
從圖中可見,InnoDB 存儲引擎由內存池,后臺線程和磁盤文件三大部分組成
再來一張突出重點的圖:
InnoDB 存儲引擎第一部分:內存結構
Buffer Pool緩沖池
InnoDB 存儲引擎基于磁盤存儲的,并將其中的記錄按照頁的方式進行管理,但是由于CPU速度和磁盤速度之間的鴻溝,基于磁盤的數據庫系統通常使用緩沖池記錄來提高數據庫的整體性能。
在數據庫進行讀取操作,將從磁盤中讀到的頁放在緩沖池中,下次再讀取相同的頁中時,首先判斷該頁是否在緩沖池中。若在緩沖池中,稱該頁在緩沖池中被命中,直接讀取該頁,否則讀取磁盤上的頁。
對于數據庫中頁的修改操作,首先修改在緩沖池中的頁,然后再以一定的頻率刷新到磁盤上,頁從緩沖池刷新回磁盤的操作并不是在每次頁發生更新時觸發,而是通過一種稱為 CheckPoint 的機制刷新回磁盤。所以,緩沖池的大小直接影響著數據庫的整體性能,可以通過配置參數 innodb_buffer_pool_size 來設置,緩沖池默認是128MB,還是有點小的,如果你的數據庫是16核32G的機器,那么你就可以給Buffer Pool分配個2GB的內存。
由于緩沖池不是無限大的,隨著不停的把磁盤上的數據頁加載到緩沖池中,緩沖池總要被用完,這個時候只能淘汰掉一些緩存頁,淘汰方式就使用最近最少被使用算法(LRU),具體來說就是引入一個新的LRU鏈表,通過這個LRU鏈表,就可以知道哪些緩存頁是最近最少被使用的,那么當你緩存頁需要騰出來一個刷入磁盤的時候,可以選擇那個LRU鏈表中最近最少被使用的緩存頁淘汰。
緩沖池中緩存的數據頁類型有:索引頁、數據頁、undo頁、插入緩沖、自適應哈希索引、InnoDB存儲的鎖信息和數據字典信息。
數據頁和索引頁
頁(Page)是 Innodb 存儲的最基本結構,也是 Innodb 磁盤管理的最小單位,與數據庫相關的所有內容都存儲在 Page 結構里。Page 分為幾種類型,數據頁和索引頁就是其中最為重要的兩種類型。
插入緩沖(Insert Buffer)
在 InnoDB 引擎上進行插入操作時,一般需要按照主鍵順序進行插入,這樣才能獲取較高的插入性能。當一張表中存在非聚簇的不唯一的索引時,在插入時,數據頁的存放還是按照主鍵進行順序存放,但是對于非聚簇索引葉子節點的插入不再是順序的了,這時就需要離散的訪問非聚簇索引頁,由于隨機讀取的存在導致插入操作性能下降。
所以 InnoDB 存儲引擎開創性地設計了 Insert Buffer ,對于非聚集索引的插入或更新操作,不是每一次直接插入到索引頁中,而是先判斷插入的非聚集索引頁是否在緩沖池中,若在,則直接插入;若不在,則先放入到一個 Insert Buffer 對象中,好似欺騙。數據庫這個非聚集的索引已經插到葉子節點,而實際并沒有,只是存放在另一個位置。然后再以一定的頻率和情況進行 Insert Buffer 和輔助索引頁子節點的 merge(合并)操作,這時通常能將多個插入合并到一個操作中(因為在一個索引頁中),這就大大提高了對于非聚集索引插入的性能。
然而 Insert Buffer 的使用需要同時滿足以下兩個條件:
索引是輔助索引( secondary index ) ;
索引不是唯一( unique )的。
當滿足以上兩個條件時, InnoDB 存儲引擎會使用 Insert Buffer ,這樣就能提高插入操作的性能了。不過考慮這樣一種情況:應用程序進行大量的插入操作,這些都涉及了不唯一的非聚集索引,也就是使用了 Insert Buffer。若此時 MySQL數據庫發生了宕機這時勢必有大量的 Insert Buffer 并沒有合并到實際的非聚集索引中去。
因此這時恢復可能需要很長的時間,在極端情況下甚至需要幾個小時。輔助索引不能是唯一的,因為在插入緩沖時,數據庫并不去查找索引頁來判斷插入的記錄的唯一性。如果去查找肯定又會有離散讀取的情況發生,從而導致 Insert Buffer 失去了意義。
可以通過命令 SHOW ENGINE INNODB STATUS 來查看插入緩沖的信息
seg size顯示了當前 Insert Buffer的大小為11336×16KB,大約為177MB; free list len代表了空閑列表的長度;size代表了已經合并記錄頁的數量。而黑體部分的第2行可能是用戶真正關心的,因為它顯示了插入性能的提高。 Inserts代表了插入的記錄數;merged recs代表了合并的插入記錄數量; merges代表合并的次數,也就是實際讀取頁的次數。 merges: merged recs大約為1:3,代表了插入緩沖將對于非聚集索引頁的離散IO邏輯請求大約降低了2/3。
正如前面所說的,目前 Insert Buffer存在一個問題是:在寫密集的情況下,插入緩沖會占用過多的緩沖池內存( innodb buffer pool),默認最大可以占用到1/2的緩沖池內存。以下是 InnoDB存儲引擎源代碼中對于 insert buffer的初始化操作:
Change Buffer
InnoDB 從1.0.x版本開始引入了 Change Buffer,可將其視為 Insert Buffer的升級版本, InnodB 存儲引擎可以對DML操作— INSERT、 DELETE、 UPDATE 都進行緩沖,他們分別是: Insert Buffer、 Delete Buffer、 Purge buffer當然和之前 Insert Buffer一樣, Change Buffer適用的對象依然是非唯一的輔助索引。
對一條記錄進行 UPDATE 操作可能分為兩個過程:
將記錄標記為已刪除;
真正將記錄刪除
因此 Delete Buffer對應 UPDATE操作的第一個過程,即將記錄標記為刪除。 PurgeBuffer對應 UPDATE 操作的第二個過程,即將記錄真正的刪除。同時, InnoDB 存儲引擎提供了參數 innodb_change_buffering,用來開啟各種Buffer的選項。該參數可選的值為: Inserts、 deletes、 purges、 changes、all、none。 Inserts、 deletes、 purges 就是前面討論過的三種情況。 changes 表示啟用 Inserts 和 deletes,all表示啟用所有,none表示都不啟用。該參數默認值為all。
從 InnoDB1.2.x版本開始,可以通過參數 innodb_change_buffer_max_size 來控制Change Buffer最大使用內存的數量:
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 值默認為25,表示最多使用1/4的緩沖池內存空間。
而需要注意的是,該參數的最大有效值為50在 MySQL5.5版本中通過命令 SHOW ENGINE INNODB STATUS,可以觀察到類似如下的內容:
可以看到這里顯示了 merged operations和 discarded operation,并且下面具體顯示 Change Buffer中每個操作的次數。 Insert 表示 Insert Buffer; delete mark表示 Delete Buffer; delete表示 Purge Buffer; discarded operations表示當 Change Buffer發生 merge時,表已經被刪除,此時就無需再將記錄合并(merge)到輔助索引中了。
自適應哈希索引
InnoDB 會根據訪問的頻率和模式,為熱點頁建立哈希索引,來提高查詢效率。InnoDB 存儲引擎會監控對表上各個索引頁的查詢,如果觀察到建立哈希索引可以帶來速度上的提升,則建立哈希索引,所以叫做自適應哈希索引。
自適應哈希索引通過緩沖池的B+樹頁構建而來,因此建立速度很快,而且不需要對整張數據表建立哈希索引。其有一個要求,即對這個頁的連續訪問模式必須一樣的,也就是說其查詢的條件必須完全一樣,而且必須是連續的。
鎖信息(lock info)
我們都知道,InnoDB 存儲引擎會在行級別上對表數據進行上鎖,不過 InnoDB 打開一張表,就增加一個對應的對象到數據字典。
數據字典
對數據庫中的數據、庫對象、表對象等的元信息的集合。在 MySQL 中,數據字典信息內容就包括表結構、數據庫名或表名、字段的數據類型、視圖、索引、表字段信息、存儲過程、觸發器等內容,MySQL INFORMATION_SCHEMA 庫提供了對數據局元數據、統計信息、以及有關MySQL Server的訪問信息(例如:數據庫名或表名,字段的數據類型和訪問權限等)。該庫中保存的信息也可以稱為MySQL的數據字典。
預讀機制
MySQL的預讀機制,就是當你從磁盤上加載一個數據頁的時候,他可能會連帶著把這個數據頁相鄰的其他數據頁,也加載到緩存里去!
舉個例子,假設現在有兩個空閑緩存頁,然后在加載一個數據頁的時候,連帶著把他的一個相鄰的數據頁也加載到緩存里去了,正好每個數據頁放入一個空閑緩存頁!
哪些情況下會觸發MySQL的預讀機制?
有一個參數是innodb_read_ahead_threshold,他的默認值是56,意思就是如果順序的訪問了一個區里的多個數據頁,訪問的數據頁的數量超過了這個閾值,此時就會觸發預讀機制,把下一個相鄰區中的所有數據頁都加載到緩存里去。
如果Buffer Pool里緩存了一個區里的13個連續的數據頁,而且這些數據頁都是比較頻繁會被訪問的,此時就會直接觸發預讀機制,把這個區里的其他的數據頁都加載到緩存里去這個機制是通過參數innodb_random_read_ahead來控制的,他默認是OFF,也就是這個規則是關閉的。
所以默認情況下,主要是第一個規則可能會觸發預讀機制,一下子把很多相鄰區里的數據頁加載到緩存里去。
預讀機制的好處為了提升性能。假設你讀取了數據頁01到緩存頁里去,那么接下來有可能會接著順序讀取數據頁01相鄰的數據頁02到緩存頁里去,這個時候,是不是可能在讀取數據頁02的時候要再次發起一次磁盤IO?
所以為了優化性能,MySQL才設計了預讀機制,也就是說如果在一個區內,你順序讀取了好多數據頁了,比如數據頁01到數據頁56都被你依次順序讀取了,MySQL會判斷,你可能接著會繼續順序讀取后面的數據頁。那么此時就提前把后續的一大堆數據頁(比如數據頁57到數據頁72)都讀取到Buffer Pool里去。
緩沖池內存管理
這里需要了解三個鏈表(Free List、Flush List、LRU List),
Free List磁盤上的數據頁和緩存頁是一 一對應起來的,都是16KB,一個數據頁對應一個緩存頁。數據庫會為Buffer Pool設計一個free鏈表,他是一個雙向鏈表數據結構,這個free鏈表里,每個節點就是一個空閑的緩存頁的描述數據塊的地址,也就是說,只要你一個緩存頁是空閑的,那么他的描述數據塊就會被放入這個free鏈表中。剛開始數據庫啟動的時候,可能所有的緩存頁都是空閑的,因為此時可能是一個空的數據庫,一條數據都沒有,所以此時所有緩存頁的描述數據塊,都會被放入這個free鏈表中,除此之外,這個free鏈表有一個基礎節點,他會引用鏈表的頭節點和尾節點,里面還存儲了鏈表中有多少個描述數據塊的節點,也就是有多少個空閑的緩存頁。
Flush List和 Free List 鏈表類似,flush鏈表本質也是通過緩存頁的描述數據塊中的兩個指針,讓被修改過的緩存頁的描述數據塊,組成一個雙向鏈表。凡是被修改過的緩存頁,都會把他的描述數據塊加入到flush鏈表中去,flush的意思就是這些都是臟頁,后續都是要flush刷新到磁盤上去。
LRU List由于緩沖池大小是一定的,換句話說 free 鏈表中的空閑緩存頁數據是一定的,當你不停的把磁盤上的數據頁加載到空閑緩存頁里去,free 鏈表中不停的移除空閑緩存頁,遲早有那么一瞬間,free 鏈表中已經沒有空閑緩存頁,這時候就需要淘汰掉一些緩存頁,那淘汰誰呢?這就需要利用緩存命中率了,緩存命中多的就是常用的,那不常用的就可以淘汰了。所以引入 LRU 鏈表來判斷哪些緩存頁是不常用的。
那LRU鏈表的淘汰策略是什么樣的呢?
假設我們從磁盤加載一個數據頁到緩存頁的時候,就把這個緩存頁的描述數據塊放到 LRU 鏈表頭部去,那么只要有數據的緩存頁,他都會在 LRU 里了,而且最近被加載數據的緩存頁,都會放到LRU鏈表的頭部去,然后加入某個緩存頁在尾部,只要發生查詢,就把它移到頭部,那么最后尾部就是需要淘汰了。
但是這樣真的就可以嗎?
第一種情況預讀機制破壞
由于預讀機制會把相鄰的沒有被訪問到的數據頁加載到緩存里,實際上只有一個緩存頁是被訪問了,另外一個通過預讀機制加載的緩存頁,其實并沒有人訪問,此時這兩個緩存頁可都在LRU鏈表的前面,如下圖
這個時候,假如沒有空閑緩存頁了,那么此時要加載新的數據頁了,是不是就要從LRU鏈表的尾部把所謂的“最近最少使用的一個緩存頁”給拿出來,刷入磁盤,然后騰出來一個空閑緩存頁了。這樣顯然是很不合理的。
第二種情況可能導致頻繁被訪問的緩存頁被淘汰的場景
全表掃描導致他直接一下子把這個表里所有的數據頁,都從磁盤加載到Buffer Pool里去。這個時候可能會一下子就把這個表的所有數據頁都一一裝入各個緩存頁里去!此時可能LRU鏈表中排在前面的一大串緩存頁,都是全表掃描加載進來的緩存頁!那么如果這次全表掃描過后,后續幾乎沒用到這個表里的數據呢?此時LRU鏈表的尾部,可能全部都是之前一直被頻繁訪問的那些緩存頁!然后當你要淘汰掉一些緩存頁騰出空間的時候,就會把LRU鏈表尾部一直被頻繁訪問的緩存頁給淘汰掉了,而留下了之前全表掃描加載進來的大量的不經常訪問的緩存頁!
優化LRU算法:基于冷熱數據分離的思想設計LRU鏈表
MySQL在設計LRU鏈表的時候,采取的實際上是冷熱數據分離的思想。LRU鏈表,會被拆分為兩個部分,一部分是熱數據,一部分是冷數據,這個冷熱數據的比例是由 innodb_old_blocks_pct 參數控制的,他默認是37,也就是說冷數據占比37%。數據頁第一次被加載到緩存的時候,實際上緩存頁會被放在冷數據區域的鏈表頭部。
然后MySQL設定了一個規則,他設計了一個 innodb_old_blocks_time 參數,默認值1000,也就是1000毫秒也就是說,必須是一個數據頁被加載到緩存頁之后,在1s之后,你訪問這個緩存頁,它會被挪動到熱數據區域的鏈表頭部去。因為假設你加載了一個數據頁到緩存去,然后過了1s之后你還訪問了這個緩存頁,說明你后續很可能會經常要訪問它,這個時間限制就是1s,因此只有1s后你訪問了這個緩存頁,他才會給你把緩存頁放到熱數據區域的鏈表頭部去。
這樣的話預讀和全表掃描的數據都只會在冷數據頭部,不會一開始就進去熱數據區。
LRU算法極致優化
LRU鏈表的熱數據區域的訪問規則優化一下,即只有在熱數據區域的后3/4部分的緩存頁被訪問了,才會給你移動到鏈表頭部去。如果你是熱數據區域的前面1/4的緩存頁被訪問,他是不會移動到鏈表頭部去的。
舉個例子,假設熱數據區域的鏈表里有100個緩存頁,那么排在前面的25個緩存頁,他即使被訪問了,也不會移動到鏈表頭部去的。但是對于排在后面的75個緩存頁,他只要被訪問,就會移動到鏈表頭部去。這樣的話,他就可以盡可能的減少鏈表中的節點移動了。
LRU鏈表淘汰緩存頁時機
MySQL在執行CRUD的時候,首先就是大量的操作緩存頁以及對應的幾個鏈表。然后在緩存頁都滿的時候,必然要想辦法把一些緩存頁給刷入磁盤,然后清空這幾個緩存頁,接著把需要的數據頁加載到緩存頁里去!
我們已經知道,他是根據LRU鏈表去淘汰緩存頁的,那么他到底是什么時候把LRU鏈表的冷數據區域中的緩存頁刷入磁盤的呢?實際上他有以下三個時機:
定時把LRU尾部的部分緩存頁刷入磁盤
后臺線程,運行一個定時任務,這個定時任務每隔一段時間就會把LRU鏈表的冷數據區域的尾部的一些緩存頁,刷入磁盤里去,清空這幾個緩存頁,把他們加入回free鏈表去。
把flush鏈表中的一些緩存頁定時刷入磁盤
如果只是把 LRU 鏈表的冷數據區域的緩存頁刷入磁盤是不夠,因為鏈表的熱數據區域里的很多緩存頁可能也會被頻繁的修改,難道他們永遠都不刷入磁盤中了嗎?
所以這個后臺線程同時也會在MySQL不怎么繁忙的時候,把flush鏈表中的緩存頁都刷入磁盤中,這樣被你修改過的數據,遲早都會刷入磁盤的!
只要flush鏈表中的一波緩存頁被刷入了磁盤,那么這些緩存頁也會從flush鏈表和lru鏈表中移除,然后加入到free鏈表中去!
所以整體效果就是不停的加載數據到緩存頁里去,不停的查詢和修改緩存數據,然后free鏈表中的緩存頁不停的在減少,flush鏈表中的緩存頁不停的在增加,lru鏈表中的緩存頁不停的在增加和移動。
另外一邊,你的后臺線程不停的在把lru鏈表的冷數據區域的緩存頁以及flush鏈表的緩存頁,刷入磁盤中來清空緩存頁,然后flush鏈表和lru鏈表中的緩存頁在減少,free鏈表中的緩存頁在增加。
free鏈表沒有空閑緩存頁
如果所有的free鏈表都被使用了,這個時候如果要從磁盤加載數據頁到一個空閑緩存頁中,此時就會從LRU鏈表的冷數據區域的尾部找到一個緩存頁,他一定是最不經常使用的緩存頁!然后把他刷入磁盤和清空,然后把數據頁加載到這個騰出來的空閑緩存頁里去!
總結一下,三個鏈表的使用情況,Buffer Pool被使用的時候,實際上會頻繁的從磁盤上加載數據頁到他的緩存頁里去,然后free鏈表、flush鏈表、lru鏈表都會同時被使用,比如數據加載到一個緩存頁,free鏈表里會移除這個緩存頁,然后lru鏈表的冷數據區域的頭部會放入這個緩存頁。
然后如果你要是修改了一個緩存頁,那么flush鏈表中會記錄這個臟頁,lru鏈表中還可能會把你從冷數據區域移動到熱數據區域的頭部去。
如果你是查詢了一個緩存頁,那么此時就會把這個緩存頁在lru鏈表中移動到熱數據區域去,或者在熱數據區域中也有可能會移動到頭部去。
Redo log Buffer 重做日志緩沖
InnoDB 有 buffer pool(簡稱bp)。bp 是數據庫頁面的緩存,對 InnoDB 的任何修改操作都會首先在bp的page上進行,然后這樣的頁面將被標記為 dirty(臟頁) 并被放到專門的 flush list 上,后續將由 master thread 或專門的刷臟線程階段性的將這些頁面寫入磁盤(disk or ssd)。
這樣的好處是避免每次寫操作都操作磁盤導致大量的隨機IO,階段性的刷臟可以將多次對頁面的修改 merge 成一次IO操作,同時異步寫入也降低了訪問的時延。然而,如果在 dirty page 還未刷入磁盤時,server非正常關閉,這些修改操作將會丟失,如果寫入操作正在進行,甚至會由于損壞數據文件導致數據庫不可用。
為了避免上述問題的發生,Innodb將所有對頁面的修改操作寫入一個專門的文件,并在數據庫啟動時從此文件進行恢復操作,這個文件就是redo log file。這樣的技術推遲了bp頁面的刷新,從而提升了數據庫的吞吐,有效的降低了訪問時延。
帶來的問題是額外的寫redo log操作的開銷(順序IO,當然很快),以及數據庫啟動時恢復操作所需的時間。
redo日志由兩部分構成:redo log buffer、redo log file(在磁盤文件那部分介紹)。innodb 是支持事務的存儲引擎,在事務提交時,必須先將該事務的所有日志寫入到 redo 日志文件中,待事務的 commit 操作完成才算整個事務操作完成。在每次將redo log buffer寫入redo log file后,都需要調用一次fsync操作,因為重做日志緩沖只是把內容先寫入操作系統的緩沖系統中,并沒有確保直接寫入到磁盤上,所以必須進行一次fsync操作。因此,磁盤的性能在一定程度上也決定了事務提交的性能(具體后面 redo log 落盤機制介紹)。
InnoDB 存儲引擎會首先將重做日志信息先放入重做日志緩沖中,然后在按照一定頻率將其刷新到重做日志文件,重做日志緩沖一般不需要設置的很大,因為一般情況每一秒鐘都會將重做日志緩沖刷新到日志文件中,可通過配置參數 Innodb_log_buffer_size 控制,默認為8MB。
Double Write 雙寫
如果說 Insert Buffer 給 InnoDB 存儲引擎帶來了性能上的提升,那么 Double wtite 帶給 InnoDB 存儲引擎的是數據頁的可靠性。
InnoDB 的 Page Size 一般是16KB,其數據校驗也是針對這16KB來計算的,將數據寫入到磁盤是以 Page 為單位進行操作的。我們知道,由于文件系統對一次大數據頁(例如InnoDB的16KB)大多數情況下不是原子操作,這意味著如果服務器宕機了,可能只做了部分寫入。16K的數據,寫入4K時,發生了系統斷電 os crash ,只有一部分寫是成功的,這種情況下就是 partial page write 問題。
有經驗的DBA可能會想到,如果發生寫失效,MySQL可以根據redo log進行恢復。這是一個辦法,但是必須清楚地認識到,redo log中記錄的是對頁的物理修改,如偏移量800,寫’aaaa’記錄。如果這個頁本身已經發生了損壞,再對其進行重做是沒有意義的。MySQL在恢復的過程中檢查page的checksum,checksum就是檢查page的最后事務號,發生partial page write問題時,page已經損壞,找不到該page中的事務號。在InnoDB看來,這樣的數據頁是無法通過 checksum 驗證的,就無法恢復。即時我們強制讓其通過驗證,也無法從崩潰中恢復,因為當前InnoDB存在的一些日志類型,有些是邏輯操作,并不能做到冪等。
為了解決這個問題,InnoDB實現了double write buffer,簡單來說,就是在寫數據頁之前,先把這個數據頁寫到一塊獨立的物理文件位置(ibdata),然后再寫到數據頁。這樣在宕機重啟時,如果出現數據頁損壞,那么在應用redo log之前,需要通過該頁的副本來還原該頁,然后再進行redo log重做,這就是double write。double write技術帶給innodb存儲引擎的是數據頁的可靠性,下面對doublewrite技術進行解析
如上圖所示,Double Write 由兩部分組成,一部分是內存中的 double write buffer,大小為2MB,另一部分是物理磁盤上共享表空間連續的128個頁,大小也為2MB。在對緩沖池的臟頁進行刷新時,并不直接寫磁盤,而是通過 memcpy 函數將臟頁先復制到內存中的該區域,之后通過 double write buffer 再分兩次,每次1MB順序地寫入共享表空間的物理磁盤上,然后馬上調用 fsync 函數,同步磁盤,避免操作系統緩沖寫帶來的問題。在完成double write 頁的寫入后,再將 double wirite buffer 中的頁寫入各個表空間文件中。
在這個過程中,doublewrite 是順序寫,開銷并不大,在完成 doublewrite 寫入后,在將 double write buffer寫入各表空間文件,這時是離散寫入。
如果操作系統在將頁寫入磁盤的過程中發生了崩潰,在恢復過程中,InnoDB 存儲引擎可以從共享表空間中的double write 中找到該頁的一個副本,將其復制到表空間文件中,再應用重做日志。
InnoDB 存儲引擎第二部分:后臺線程
IO 線程
在 InnoDB 中使用了大量的 AIO(Async IO) 來做讀寫處理,這樣可以極大提高數據庫的性能。在 InnoDB 1.0 版本之前共有4個 IO Thread,分別是 write,read,insert buffer和log thread,后來版本將 read thread和 write thread 分別增大到了4個,一共有10個了。
- read thread : 負責讀取操作,將數據從磁盤加載到緩存page頁。4個
- write thread:負責寫操作,將緩存臟頁刷新到磁盤。4個
- log thread:負責將日志緩沖區內容刷新到磁盤。1個
- insert buffer thread :負責將寫緩沖內容刷新到磁盤。1個
Purge 線程
事務提交之后,其使用的 undo 日志將不再需要,因此需要 Purge Thread 回收已經分配的 undo 頁。show variables like '%innodb*purge*threads%';
Page Cleaner 線程
作用是將臟數據刷新到磁盤,臟數據刷盤后相應的 redo log 也就可以覆蓋,即可以同步數據,又能達到 redo log 循環使用的目的。會調用write thread線程處理。show variables like '%innodb*page*cleaners%';
InnoDB 存儲引擎第三部分:磁盤文件
InnoDB 的主要的磁盤文件主要分為三大塊:一是系統表空間,二是用戶表空間,三是 redo 日志文件和歸檔文件。
二進制文件(binlong)等文件是 MySQL Server 層維護的文件,所以未列入 InnoDB 的磁盤文件中。
系統表空間和用戶表空間
系統表空間包含 InnoDB 數據字典(元數據以及相關對象)并且 double write buffer , change buffer , undo logs 的存儲區域。
系統表空間也默認包含任何用戶在系統表空間創建的表數據和索引數據。
系統表空間是一個共享的表空間,因為它是被多個表共享的。
系統表空間是由一個或者多個數據文件組成。默認情況下,1個初始大小為10MB,名為 ibdata1 的系統數據文件在MySQL的data目錄下被創建。用戶可以使用 innodb_data_file_path 對數據文件的大小和數量進行配置。
innodb_data_file_path 的格式如下:
innodb_data_file_path=datafile1[,datafile2]...
用戶可以通過多個文件組成一個表空間,同時制定文件的屬性:
innodb_data_file_path = /db/ibdata1:1000M;/dr2/db/ibdata2:1000M:autoextend
這里將 /db/ibdata1 和 /dr2/db/ibdata2 兩個文件組成系統表空間。如果這兩個文件位于不同的磁盤上,磁盤的負載可能被平均,因此可以提高數據庫的整體性能。兩個文件的文件名之后都跟了屬性,表示文件 ibdata1 的大小為1000MB,文件 ibdata2 的大小為1000MB,而且用完空間之后可以自動增長。
設置 innodb_data_file_path 參數之后,所有基于 InnoDB 存儲引擎的表的數據都會記錄到該系統表空間中,如果設置了參數 innodb_file_per_table ,則用戶可以將每個基于 InnoDB 存儲引擎的表產生一個獨立的用戶空間。
用戶表空間的命名規則為:表名.ibd。通過這種方式,用戶不用將所有數據都存放于默認的系統表空間中,但是用戶表空間只存儲該表的數據、索引和插入緩沖BITMAP等信息,其余信息還是存放在默認的系統表空間中。
下圖顯示 InnoDB 存儲引擎對于文件的存儲方式,其中frm文件是表結構定義文件,記錄每個表的表結構定義。
重做日志文件(redo log file)和歸檔文件
默認情況下,在 InnoDB 存儲引擎的數據目錄下會有兩個名為 ib_logfile0 和 ib_logfile1 的文件,這就是 InnoDB 的重做文件(redo log file),它記錄了對于 InnoDB 存儲引擎的事務日志。
當 InnoDB 的數據存儲文件發生錯誤時,重做日志文件就能派上用場。InnoDB 存儲引擎可以使用重做日志文件將數據恢復為正確狀態,以此來保證數據的正確性和完整性。
每個 InnoDB 存儲引擎至少有1個重做日志文件,每個文件組下至少有2個重做日志文件,加默認的 ib_logfile0 和 ib_logfile1。
為了得到更高的可靠性,用戶可以設置多個鏡像日志組,將不同的文件組放在不同的磁盤上,以此來提高重做日志的高可用性。
在日志組中每個重做日志文件的大小一致,并以【循環寫入】的方式運行。InnoDB 存儲引擎先寫入重做日志文件1,當文件被寫滿時,會切換到重做日志文件2,再當重做日志文件2也被寫滿時,再切換到重做日志1。
用戶可以使用 Innodb_log_file_size 來設置重做日志文件的大小 ,這對 InnoDB 存儲引擎的性能有著非常大的影響。
如果重做日志文件設置的太大,數據丟失時,恢復時可能需要很長的時間;另一個方面,如果設置的太小,重做日志文件太小會導致依據 checkpoint 的檢查需要頻繁刷新臟頁到磁盤中,導致性能的抖動。
重做日志的落盤機制
InnoDB 對于數據文件和日志文件的刷盤遵守WAL(write ahead redo log)和 Force-log-at-commit 兩種規則,二者保證了事務的持久性。WAL 要求數據的變更寫入到磁盤前,首先必須將內存中的日志寫入到磁盤;Force-log-at-commit 要求當一個事務提交時,所有產生的日志都必須刷新到磁盤上,如果日志刷新成功后,緩沖池中的數據刷新到磁盤前數據庫發生了宕機,那么重啟時,數據庫可以從日志中恢復數據。
如上圖所示,InnoDB 在緩沖池中變更數據時,會首先將相關變更寫入重做日志緩沖中,然后再按時(比如每秒刷新機制)或者當事務提交時寫入磁盤,這符合 Force-log-at-commit 原則;當重做日志寫入磁盤后,緩沖池中的變更數據才會依據 checkpoint 機制寫入到磁盤中,這符合 WAL 原則。
在 checkpoint 擇時機制中,就有重做日志文件寫滿的判斷,所以,如前文所述,如果重做日志文件太小,經常被寫滿,就會頻繁導致 checkpoint 將更改的數據寫入磁盤,導致性能抖動。
操作系統的文件系統是帶有緩存的,當 InnoDB 向磁盤寫入數據時,有可能只是寫入到了文件系統的緩存中,沒有真正的“落袋為安”。
InnoDB 的 innodb_flush_log_at_trx_commit 屬性可以控制每次事務提交時 InnoDB 的行為。當屬性值為0時,事務提交時,不會對重做日志進行寫入操作,而是等待主線程按時寫入;當屬性值為1時,事務提交時,會將重做日志寫入文件系統緩存,并且調用文件系統的 fsync ,將文件系統緩沖中的數據真正寫入磁盤存儲,確保不會出現數據丟失;當屬性值為2時,事務提交時,也會將日志文件寫入文件系統緩存,但是不會調用fsync,而是讓文件系統自己去判斷何時將緩存寫入磁盤。
日志的刷盤機制如下圖所示:
Innodb_flush_log_at_commit 是 InnoDB 性能調優的一個基礎參數,涉及 InnoDB 的寫入效率和數據安全。當參數數值為0時,寫入效率最高,但是數據安全最低;參數值為1時,寫入效率最低,但是數據安全最高;參數值為2時,二者都是中等水平,一般建議將屬性值設置為1,以獲得較高的安全性,而且也只有設置為1,才能保證事務的持久性。
用一條 UPDATE 語句再來了解 InnoDB 存儲引擎
有了上面 InnoDB 存儲引擎的架構基礎介紹,我們再來分析一下一次 UPDATE 數據更新具體流程。
我們把這張圖分為上下兩部分來看,上面那部分是 MySQL Server 層處理流程,下面那部分是 MySQL InnoDB存儲引擎處理流程。
MySQL Server 層處理流程
這部分處理流程無關于哪個存儲引擎,它是 Server 層處理的,具體步驟如下:
用戶各種操作觸發后臺sql執行,通過web項目中自帶的數據庫連接池:如 dbcp、c3p0、druid 等,與數據庫服務器的數據庫連接池建立網絡連接;
數據庫連接池中的線程監聽到請求后,將接收到的sql語句通過SQL接口響應給查詢解析器,查詢解析器將sql按照sql的語法解析出查詢哪個表的哪些字段,查詢條件是啥;
再通過查詢優化器處理,選擇該sq最優的一套執行計劃;
然后執行器負責調用存儲引擎的一系列接口,執行該計劃而完成整個sql語句的執行
這部分流程和上面分析的 一次 Select 請求處理流程分析的基本一致。
InnoDB 存儲引擎處理流程
具體執?語句得要存儲引擎來完成,如上圖所示:
更新users表中id=10的這條數據,如果緩沖池中沒有該條數據的,得要先從磁盤中將被更新數據的原始數據加載到緩沖池中。
同時為了保證并發更新數據安全問題,會對這條數據先加鎖,防?其他事務進?更新。
接著將更新前的值先備份寫?到undo log中(便于事務回滾時取舊數據),?如 update 語句即存儲被更新字段之前的值。
更新 buffer pool 中的緩存數據為最新的數據,那么此時內存中的數據為臟數據(內存中數據和磁盤中數據不一致)
?此就完成了在緩沖池中的執?流程(如上圖)。
緩沖池中更新完數據后,需要將本次的更新信息順序寫到 Redo Log ?志,因為現在已經把內存里的數據進行了修改,但是磁盤上的數據還沒修改,此時萬一 MySQL所在的機器宕機了,必然會導致內存里修改過的數據丟失,redo 日志就是記錄下來你對數據做了什么修改,比如對“id=10這行記錄修改了name字段的值為xxx”,這就是一個日志,用來在MySQL突然宕機的時候,用來恢復你更新過的數據的。不過注意的是此時 Redo Log 還沒有落盤到日志文件。
這個時候思考一個問題:如果還沒提交事務,MySQL宕機了怎么辦?
上面我們知道到目前我們修改了內存數據,然后記錄了 Redo Log Buffer 日志緩沖,如果這個時候 MySQL 奔潰,內存數據和 Redo Log Buffer 數據都會丟失,但是此時數據丟失并不要緊,因為一條更新語句,沒提交事務,就代表他沒執行成功,此時MySQL宕機雖然導致內存里的數據都丟失了,但是你會發現,磁盤上的數據依然還停留在原樣子。
接下來要提交事物了,此時就會根據一定的策略把redo日志從redo log buffer里刷入到磁盤文件里去,此時這個策略是通過 innodb_flush_log_at_trx_commit 來配置的。
innodb_flush_log_at_trx_commit=0,表示提交事物不會把redo log buffer里的數據刷入磁盤文件的,此時可能你都提交事務了,結果mysql宕機了,然后此時內存里的數據全部丟失,所以這種方式不可取。
innodb_flush_log_at_trx_commit=1,redo log從內存刷入到磁盤文件里去,只要事務提交成功,那么redo log就必然在磁盤里了,所以如果這個時候MySQL奔潰,可以根據Redo Log日志恢復數據。
innodb_flush_log_at_trx_commit=2,提交事務的時候,把redo日志寫入磁盤文件對應的os cache緩存里去,而不是直接進入磁盤文件,可能1秒后才會把os cache里的數據寫入到磁盤文件里去。
提交事務的時候,同時會寫入binlog,binlog也有不同的刷盤策略,有一個sync_binlog參數可以控制binlog的刷盤策略,他的默認值是0,此時你把binlog寫入磁盤的時候,其實不是直接進入磁盤文件,而是進入os cache內存緩存。?般我們為了保證數據不丟失會配置雙1策略,Redo Log 和 Binlog落盤策略都選擇1。
Binlog 落盤后,再將Binlog的?件名、?件所在路徑信息以及commit標記給同步順序寫到Redo log中,這一步的意義是用來保持 redo log 日志與 binlog 日志一致的。commit標記是判定事務是否成功提交的?個?較重要的標準,舉個例子,如果如果第5步或者第6步執行成功后MySQL就奔潰了,這個時候因為沒有最終的事務commit標記在redo日志里,所以此次事務可以判定為不成功。不會說redo日志文件里有這次更新的日志,但是binlog日志文件里沒有這次更新的日志,不會出現數據不一致的問題。
做完前面后,內存數據已經修改,事物已經提交,日志已經落盤,但是磁盤數據還沒有同步修改。InnoDB存儲引擎后臺有?個IO線程,會在數據庫壓?的低峰期間,將緩沖池中被事務更新、但還沒來得及寫到磁盤中的數據(臟數據,因為磁盤數據和內存數據已經不?致了)給刷到磁盤中,完成事務的持久化。
所以 InnoDB 處理寫入過程可以用下面這幅圖表示
到此,關于“mysql體系結構和InnoDB存儲引擎知識有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。