上篇文章介紹了一條SQL語句在數(shù)據(jù)庫中的執(zhí)行流程,當(dāng)SQL執(zhí)行到存儲引擎這里,因為不同的引擎的實現(xiàn)機制有所不同,現(xiàn)在就以使用最廣泛的為InnoDB引擎再來細化說明Innodb在數(shù)據(jù)查詢和更新流程的細節(jié)。
上篇: Mysql快速學(xué)習(xí)——《一》: Mysql的基礎(chǔ)架構(gòu)
參考: The InnoDB Storage Engine
思維導(dǎo)圖: mysql
存儲引擎層
存儲引擎層(Storage Engines),它決定了 MySQL 會怎樣存儲數(shù)據(jù),怎樣讀取和寫入數(shù)據(jù),也在很大程度上決定了 MySQL 的讀寫性能和數(shù)據(jù)可靠性。
對于這么重要的一層能力,MySQL 提供了極強的擴展性,你可以定義自己要使用什么樣的存儲引擎:InnoDB、MyISAM、MEMORY、CSV,甚至可以自己開發(fā)一個存儲引擎然后使用它。
InnoDB存儲引擎
下面是InnoDB 引擎的邏輯架構(gòu)圖, 弄懂它那你對MYSQL的理解將更進一步:
innoDB架構(gòu)圖從上圖可以看到InnoDB分為2大塊:
- In-Memory Structures (內(nèi)存部分)
- On-Disk Structures (磁盤部分)
其中In-Memory Structures (內(nèi)存部分)包括:
- Buffer Pool - 內(nèi)存緩沖池
- Change Buffer - 寫交換緩沖池
- Adaptive Hash Index - 自適應(yīng)哈希索引
- Log Buffer
而On-Disk Structures (磁盤部分), 從架構(gòu)圖可以看到, Tablespaces 分為五種(我們平時創(chuàng)建的表的數(shù)據(jù),可以存放到 The System Tablespace 、File-Per-Table Tablespaces、General Tablespace 三者中的任意一個地方,具體取決于你的配置和創(chuàng)建表時的 sql 語句)
- The System Tablespace - 系統(tǒng)表空間
---- 在數(shù)據(jù)庫建立的時候自動創(chuàng)建的,它包含了整個數(shù)據(jù)庫的數(shù)據(jù)字典。- File-Per-Table Tablespaces - 獨立表空間
---- 是對The System Tablespace(系統(tǒng)表空間)的一個更靈活的選擇, 在MySQL 5.6.6和更高版本默認啟用的。- General Tablespace - 通用表空間
--- 類似于系統(tǒng)表空間,常規(guī)表空間是共享表空間,可以存儲多個表的數(shù)據(jù)。- Undo Tablespaces - 回退表空間
- Temporary Tablespaces
On-Disk Structures (磁盤部分)除了表結(jié)構(gòu)定義和索引,還有一些為了高性能和高可靠而設(shè)計的角色,比如
- redo log
- undo log
- Change Buffer
- Doublewrite Buffer
內(nèi)存部分組件詳解
1. Buffer Pool
緩存表數(shù)據(jù)與索引數(shù)據(jù),把磁盤上的數(shù)據(jù)加載到緩沖池,避免每次訪問都進行磁盤IO,起到加速訪問的作用。
InnoDB緩沖池策略
- 按頁(4K)讀取
磁盤讀寫,并不是按需讀取,而是按頁讀取,一次至少讀一頁數(shù)據(jù)(一般是4K),如果未來要讀取的數(shù)據(jù)就在頁中,就能夠省去后續(xù)的磁盤IO,提高效率。- “集中讀寫”的原則(預(yù)讀)
數(shù)據(jù)訪問,通常都遵循“集中讀寫”的原則,使用一些數(shù)據(jù),大概率會使用附近的數(shù)據(jù),這就是所謂的“局部性原理”。InnoDB會把一些“可能要訪問”的頁提前加入緩沖池,避免未來的磁盤IO操作。
傳統(tǒng)LRU緩沖池算法
為了減少數(shù)據(jù)移動,LRU一般用鏈表實現(xiàn)。最常見的玩法是,把入緩沖池的頁放到LRU的頭部,作為最近訪問的元素,從而最晚被淘汰。這里又分兩種情況:
(1)頁已經(jīng)在緩沖池里,那就只做“移至”LRU頭部的動作,而沒有頁被淘汰;
(2)頁不在緩沖池里,除了做“放入”LRU頭部的動作,還要做“淘汰”LRU尾部頁的動作;InnoDB并不直接使用傳統(tǒng)的LRU緩沖池算法, 因為傳統(tǒng)的LRU緩沖池算法會出現(xiàn)以下問題:
(1)預(yù)讀失效: 由于預(yù)讀(Read-Ahead),提前把頁放入了緩沖池,但最終MySQL并沒有從頁中讀取數(shù)據(jù),稱為預(yù)讀失效。
(2)緩沖池污染: 當(dāng)某一個SQL語句,要批量掃描大量數(shù)據(jù)時,可能導(dǎo)致把緩沖池的所有頁都替換出去,導(dǎo)致大量熱數(shù)據(jù)被換出,MySQL性能急劇下降,這種情況叫緩沖池污染。
InnoDB對傳統(tǒng)LRU旳優(yōu)化
預(yù)讀失敗優(yōu)化 - 新老生代機制
(1)將LRU分為兩個部分:新生代(new sublist) + 老生代(old sublist)
(2)新老生代收尾相連,即:新生代的尾(tail)連接著老生代的頭(head);
(3)新頁(例如被預(yù)讀的頁)加入緩沖池時,只加入到老生代頭部
(4)如果數(shù)據(jù)真正被讀取(預(yù)讀成功),才會加入到新生代的頭部
(5)如果數(shù)據(jù)沒有被讀取,則會比新生代里的“熱數(shù)據(jù)頁”更早被淘汰出緩沖池
新老生代機制
緩沖池污染優(yōu)化 - 老生代停留時間窗口機制
(1)假設(shè)T=老生代停留時間窗口;
(2)插入老生代頭部的頁,即使立刻被訪問,并不會立刻放入新生代頭部;
(3)只有滿足“被訪問”并且“在老生代停留時間”大于T,才會被放入新生代頭部;
老生代停留時間窗口機制
buffer_pool相關(guān)重要參數(shù)
- innodb_buffer_pool_size
配置緩沖池的大小,在內(nèi)存允許的情況下,DBA往往會建議調(diào)大這個參數(shù),越多數(shù)據(jù)和索引放到內(nèi)存里,數(shù)據(jù)庫的性能會越好。- innodb_old_blocks_pct
老生代占整個LRU鏈長度的比例,默認是37,即整個LRU中新生代與老生代長度比例是63:37。- innodb_old_blocks_time
老生代停留時間窗口,單位是毫秒,默認是1000,即同時滿足“被訪問”與“在老生代停留時間超過1秒”兩個條件,才會被插入到新生代頭部。
2. Change Buffer
寫請求的處理流程
(1)如果索引頁不在buffer pool中, 則先把索引頁,從磁盤加載到緩沖池,一次磁盤隨機讀操作;
(2)修改緩沖池中的頁,一次內(nèi)存操作;
(3)寫入redo log,一次磁盤順序?qū)懖僮鳎?/p>
寫請求處理
是否會出現(xiàn)一致性問題呢?
不會, 因為:
(1)讀取,會命中緩沖池的頁;
(2)緩沖池LRU數(shù)據(jù)淘汰,會將“臟頁”刷回磁盤;
(3)數(shù)據(jù)庫異常奔潰,能夠從redo log中恢復(fù)數(shù)據(jù);
利用Change Buffer進行優(yōu)化
上述場景中, 被讀取的數(shù)據(jù)沒有命中緩沖池的時候,會先從磁盤索引頁到緩沖池中, 這樣至少產(chǎn)生一次磁盤IO,對于寫多讀少的業(yè)務(wù)場景,性能壓力會劇增, 于是InnoDB引入了Change Buffer:
- 當(dāng)對頁進行了寫操作,并不會立刻將磁盤頁加載到緩沖池
- 先把頁的寫操作記錄到緩沖變更池(buffer changes)
- 等未來數(shù)據(jù)被讀取時,再將數(shù)據(jù)合并(merge)恢復(fù)到緩沖池中
寫緩沖的目的是降低寫操作的磁盤IO,提升數(shù)據(jù)庫性能。
Change Buffer在內(nèi)存中,Change Buffer占用Buffer Pool的一部分。在磁盤上,Change Buffer是系統(tǒng)表空間的一部分,其中的索引會在關(guān)閉數(shù)據(jù)庫服務(wù)器時更改。
Change Buffer相關(guān)參數(shù)配置
- 配置Change Pool最大大小
---innodb_change_buffer_max_size: 允許將Change Buffer的最大大小配置為緩沖池總大小的百分比。默認情況下, innodb_change_buffer_max_size設(shè)置為25.最大設(shè)置為50。- 配置Change Buffer的適用范圍
---innodb_change_buffering: all | none | inserts | deletes | changes | purges
3. Adaptive Hash Index
AHI是InnoDB索引的索引, 為了在索引很大時快速得到數(shù)據(jù)。
AHI 在實現(xiàn)上就是一個哈希表:從某個檢索條件到某個數(shù)據(jù)頁的哈希表,仿佛并不復(fù)雜,但其中的關(guān)竅在于哈希表不能太大(哈希表維護本身就有成本,哈希表太大則成本會高于收益),又不能太小(太小則緩存命中率太低,沒有任何收益)。
AHI建立需要遵循以下約束:
(1)某個索引樹要被使用足夠多次
(2)該索引樹上的某個檢索條件要被經(jīng)常使用
(3)該索引樹上的某個數(shù)據(jù)頁要被經(jīng)常使用
4. Log Buffer
當(dāng)在MySQL中對InnoDB表進行更改時,這些更改首先存儲在InnoDB日志緩沖區(qū)的內(nèi)存中,然后寫入通常稱為重做日志(redo logs)的InnoDB日志文件中。
日志緩沖區(qū)是內(nèi)存存儲區(qū)域,用于保存要寫入磁盤上的日志文件的數(shù)據(jù)。日志緩沖區(qū)大小由innodb_log_buffer_size 變量定義,默認大小為16MB。
日志緩沖區(qū)的內(nèi)容定期刷新到磁盤。較大的日志緩沖區(qū)可以運行大型事務(wù),而無需在事務(wù)提交之前將重做日志數(shù)據(jù)寫入磁盤。因此,如果有更新,插入或刪除許多行的事務(wù),則增加日志緩沖區(qū)的大小可以節(jié)省磁盤I/O。
磁盤部分組件詳解
1. redo log
在更新操作時,會先更新Buffer Pool中的數(shù)據(jù)然后再去操作磁盤,但是在極端情況下會出現(xiàn)系統(tǒng)宕機或者斷電導(dǎo)致磁盤還未更新就丟失了數(shù)據(jù),此時需要把對內(nèi)存所做的修改寫入到一個redo log buffer里去,這里也是一個內(nèi)存緩沖區(qū),用于存放redo日志的。
2. undo log
如果執(zhí)行一個更新語句,且這個語句還在事務(wù)里的話,在事務(wù)提交以前,我們都可以選擇回滾,而這部分回滾的數(shù)據(jù),就是未更新以前的數(shù)據(jù),它是保存在undo日志里的。
3. Change Buffer
--
4. Doublewrite Buffer
如果說 Change Buffer 是提升性能,那么 Doublewrite Buffer 就是保證數(shù)據(jù)頁的可靠性。
怎么理解呢?
前面提到過,MySQL 以「頁」為讀取和寫入單位,一個「頁」里面有多行數(shù)據(jù),寫入數(shù)據(jù)時,MySQL 會先寫內(nèi)存中的頁,然后再刷新到磁盤中的頁。
這時問題來了,假設(shè)在某一次從內(nèi)存刷新到磁盤的過程中,一個「頁」刷了一半,突然操作系統(tǒng)或者 MySQL 進程奔潰了,這時候,內(nèi)存里的頁數(shù)據(jù)被清除了,而磁盤里的頁數(shù)據(jù),刷了一半,處于一個中間狀態(tài),不尷不尬,可以說是一個「不完整」,甚至是「壞掉的」的頁。有同學(xué)說,不是有 Redo Log 么?其實這個時候 Redo Log 也已經(jīng)無力回天,Redo Log 是要在磁盤中的頁數(shù)據(jù)是正常的、沒有損壞的情況下,才能把磁盤里頁數(shù)據(jù) load 到內(nèi)存,然后應(yīng)用 Redo Log。而如果磁盤中的頁數(shù)據(jù)已經(jīng)損壞,是無法應(yīng)用 Redo Log 的。
所以,MySQL 在刷數(shù)據(jù)到磁盤之前,要先把數(shù)據(jù)寫到另外一個地方,也就是 Doublewrite Buffer,寫完后,再開始寫磁盤。Doublewrite Buffer 可以理解為是一個備份(recovery),萬一真的發(fā)生 crash,就可以利用 Doublewrite Buffer 來修復(fù)磁盤里的數(shù)據(jù)。
代碼塊