??????
? ??????lnnoDB是事務(wù)安全的MySQL存儲(chǔ)引擎, 設(shè)計(jì)上采用了類(lèi)似于Oracle數(shù)據(jù)庫(kù)的架構(gòu)。 通常來(lái)說(shuō),InnoDB存儲(chǔ)引擎是OLTP應(yīng)用中核心表的首選存儲(chǔ)引擎 。同時(shí), 也正是因?yàn)镮nnoDB的存在, 才使MySQL數(shù)據(jù)庫(kù)變得更有魅力。 本章將詳細(xì)介紹lnnoDB存儲(chǔ)引擎的體系架構(gòu)及其不同于其他存儲(chǔ)引擎的特性。
2.1 InnoDB存儲(chǔ)引擎概述
????????lnnoDB存儲(chǔ)引擎特點(diǎn)是行鎖設(shè)計(jì)、 支持MVCC、 支持外鍵、 提供一致性非鎖定讀, 同時(shí)被設(shè)計(jì)用來(lái)最有效地利用以及使用內(nèi)存和CPU。
2.3 lnnoDB體系架構(gòu)
????????圖2-1簡(jiǎn)單顯示了InnoDB的存儲(chǔ)引擎的體系架構(gòu), 從圖可見(jiàn),lnnoDB存儲(chǔ)引擎有多個(gè)內(nèi)存塊, 可以認(rèn)為這些內(nèi)存塊組成了一個(gè)大的內(nèi)存池, 負(fù)責(zé)如下工作:
1,維護(hù)所有進(jìn)程/線(xiàn)程需要訪(fǎng)問(wèn)的多個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
2,緩存磁盤(pán)上的數(shù)據(jù), 方便快速地讀取, 同時(shí)在對(duì)磁盤(pán)文件的數(shù)據(jù)修改之前在這里緩存。
3,重做日志(redo log)緩沖。
????????后臺(tái)線(xiàn)程的主要作用是負(fù)責(zé)刷新內(nèi)存池中的數(shù)據(jù), 保證緩沖池中的內(nèi)存緩存的是最近的數(shù)據(jù)。此外將已修改的數(shù)據(jù)文件刷新到磁盤(pán)文件, 同時(shí)保證在數(shù)據(jù)庫(kù)發(fā)生異常的情況下InnoDB能恢復(fù)到正常運(yùn)行狀態(tài)。
2.3.1 后臺(tái)線(xiàn)程
????????InnoDB存儲(chǔ)引擎是多線(xiàn)程的模型, 因此其后臺(tái)有多個(gè)不同的后臺(tái)線(xiàn)程, 負(fù)責(zé)處理不同的任務(wù)。
1 . Master Thread
? ??????Master Thread是一個(gè)非常核心的后臺(tái)線(xiàn)程, 主要負(fù)責(zé)將緩沖池中的數(shù)據(jù)異步刷新到磁盤(pán), 保證數(shù)據(jù)的一致性, 包括臟頁(yè)的刷新、 合并插入緩沖(INSERT BUFFER)、 UNDO頁(yè)的回收等。2.5節(jié)會(huì)詳細(xì)地介紹各個(gè)版本中Master Thread的工作方式。
2.IO Thread
? ??????在InnoDB存儲(chǔ)引擎中大量使用了AIO(Async IO)來(lái)處理寫(xiě)IO請(qǐng)求, 這樣可以極大提高數(shù)據(jù)庫(kù)的性能。而IO Thread的工作主要是負(fù)責(zé)這些IO請(qǐng)求的回調(diào)(call back) 處理。
3. Purge Thread
? ??????事務(wù)被提交后, 其所使用的undolog可能不再需要, 因此需要PurgeThread來(lái)回收已經(jīng)使用并分配的undo頁(yè)。 在InnoDB 1.1版本之前,purge操作僅在InnoDB存儲(chǔ)引擎的Master Thread中完成。 而從InnoDB 1.1版本開(kāi)始,purge操作可以獨(dú)立到單獨(dú)的線(xiàn)程中進(jìn)行, 以此來(lái)減輕Master Thread的工作, 從而提高CPU的使用率以及提升存儲(chǔ)引擎的性能。
? ??????從InnoDB 1.2版本開(kāi)始,InnoDB支持多個(gè)Purge Thread, 這樣做的目的是為了進(jìn)一步步加快undo頁(yè)的回收。同時(shí)由于Purge Thread需要離散地讀取undo頁(yè), 這樣也能更進(jìn)一步利用磁盤(pán)的隨機(jī)讀取性能。
#undo log有兩個(gè)作用:提供回滾和多個(gè)行版本控制(MVCC)。
在數(shù)據(jù)修改的時(shí)候,不僅記錄了redo,還記錄了相對(duì)應(yīng)的undo,如果因?yàn)槟承┰驅(qū)е率聞?wù)失敗或回滾了,可以借助該undo進(jìn)行回滾。
4. Page Cleaner Thread
? ??????Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是將之前版本中臟頁(yè)的刷新操作都放入到單獨(dú)的線(xiàn)程中來(lái)完成。而其目的是為了減輕原Master Thread的工作及對(duì)于用戶(hù)查詢(xún)線(xiàn)程的阻塞, 進(jìn)一步提高InnoDB存儲(chǔ)引擎的性能。
2.3.2 內(nèi)存
1. 緩沖池
? ??????lnnoDB存儲(chǔ)引擎是基于磁盤(pán)存儲(chǔ)的, 并將其中的記錄按照頁(yè)的方式進(jìn)行管理。因此可將其視為基于磁盤(pán)的數(shù)據(jù)庫(kù)系統(tǒng)(Disk-base Database)。在數(shù)據(jù)庫(kù)系統(tǒng)中, 由于CPU速度與磁盤(pán)速度之間的鴻溝, 基于磁盤(pán)的數(shù)據(jù)庫(kù)系統(tǒng)通常使用緩沖池技術(shù)來(lái)提高數(shù)據(jù)庫(kù)的整體性能。
? ??????緩沖池簡(jiǎn)單來(lái)說(shuō)就是一塊內(nèi)存區(qū)域, 通過(guò)內(nèi)存的速度來(lái)彌補(bǔ)磁盤(pán)速度較慢對(duì)數(shù)據(jù)庫(kù)性能的影響。在數(shù)據(jù)庫(kù)中進(jìn)行讀取頁(yè)的操作, 首先將從磁盤(pán)讀到的頁(yè)存放在緩沖池中, 這個(gè)過(guò)程稱(chēng)為將頁(yè)"FIX"在緩沖池中。下一次再讀相同的頁(yè)時(shí),首先判斷該頁(yè)是否在 緩沖池中。若在緩沖池中, 稱(chēng)該頁(yè)在緩沖池中被命中, 直接讀取該頁(yè)。否則, 讀取磁盤(pán)上的頁(yè)。
? ??????對(duì)于數(shù)據(jù)庫(kù)中頁(yè)的修改操作, 則首先修改在緩沖池中的頁(yè), 然后再以一定的頻率刷新到磁盤(pán)上。這里需要注意的是, 頁(yè)從緩沖池刷新回磁盤(pán)的操作并不是在每次頁(yè)發(fā)生更新時(shí)觸發(fā),而是通過(guò)一種稱(chēng)為Checkpoint的機(jī)制刷新回磁盤(pán)。同樣,這也是為了提高數(shù)據(jù)庫(kù)的整體性能。
? ??????綜上所述,緩沖池的大小直接影響指數(shù)據(jù)庫(kù)的整體性能。由于32位操作系統(tǒng)的限制,在該系統(tǒng)下最多將該值設(shè)置為3G。此外用戶(hù)可以打開(kāi)操作系統(tǒng)的PAE選項(xiàng)來(lái)獲得 32位操作系統(tǒng)下最大64GB內(nèi)存的 支持。隨著內(nèi)存技術(shù)的不斷成熟,其成本也在不斷下降。單條8GB的內(nèi)存變得非常普遍,而PC服務(wù)器已經(jīng)能支持512GB的內(nèi)存 。因此為了讓數(shù)據(jù)庫(kù)使用更多的內(nèi)存,強(qiáng)烈建議數(shù)據(jù)庫(kù)服務(wù)器都采用64位的操作系統(tǒng)。
? ??????對(duì)于InnoDB存儲(chǔ)引擎而言,其緩沖池的配置通過(guò)參數(shù)innodb_ buffer _pool_ size來(lái)設(shè)置。
? ??????具體來(lái)看,緩沖池中緩存的數(shù)據(jù)頁(yè)類(lèi)型有:索引頁(yè)、數(shù)據(jù)頁(yè)、undo頁(yè)、插入緩沖(insert buffer)、自適應(yīng)哈希索引(adaptive hash index)、InnoDB存儲(chǔ)的鎖信息(lockinfo)、數(shù)據(jù)字典信息(data dictionary)等。不能簡(jiǎn)單地認(rèn)為,緩沖池只是緩存索引頁(yè)和數(shù)據(jù)頁(yè),它們只是占緩沖池很大的一部分而已。圖2-2很好地顯示了InnoDB存儲(chǔ)引擎中內(nèi)存的結(jié)構(gòu)情況。
????????從InnoDB 1.0.x版本開(kāi)始,允許有多個(gè)緩沖池實(shí)例。每個(gè)頁(yè)根據(jù)哈希值平均分配到不同緩沖池實(shí)例中 。這樣做的好處是減少數(shù)據(jù)庫(kù)內(nèi)部的資源競(jìng)爭(zhēng),增加數(shù)據(jù)庫(kù)的并發(fā)處理能力。可以通過(guò)參數(shù)innodb_ buffer _pool_ instances來(lái)進(jìn)行配置, 該值默認(rèn)為1。
2. LRU List、Free List和Flush List
????????通常來(lái)說(shuō), 數(shù)據(jù)庫(kù)中的緩沖池是通過(guò)LRU (Latest Recent Used, 最近最少使用)算法來(lái)進(jìn)行管理的。 即最頻繁使用的頁(yè)在LRU列表的 前端, 而最少使用的頁(yè)在LRU列表的尾端。 當(dāng)緩沖池不能存放新讀取到的頁(yè)時(shí), 將首先釋放LRU列表中尾端的頁(yè)。
? ??????在InnoDB存儲(chǔ)引擎中, 緩沖池中頁(yè)的大小默認(rèn)為16KB, 同樣使用LRU算法對(duì)緩沖池進(jìn)行管理。稍有不同的是InnoDB存儲(chǔ)引擎對(duì)傳統(tǒng)的LRU算法做了一些優(yōu)化,在InnoDB的存儲(chǔ)引擎中,LRU列表中還加入了midpoint位置。新讀取到的頁(yè),雖然是最新訪(fǎng)問(wèn)的頁(yè),但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。這個(gè)算法在 lnnoDB存儲(chǔ)引擎下稱(chēng)為midpoint insertion strategy。在默認(rèn)配置下, 該位置在LRU列表長(zhǎng)度的5/8處。midpoint位置可由參數(shù)innodb _old_ blocks _pct控制
? ??????那為什么不采用樸素的LRU算法 ,直接將讀取的頁(yè)放入到LRU列表的首部呢?這是因?yàn)槿糁苯訉⒆x取到的頁(yè)放入到LRU的首部, 那么某些SQL操作可能會(huì)使緩沖池中的頁(yè)被刷新出,從而影響緩沖池的效率。常見(jiàn)的這類(lèi)操作為索引或數(shù)據(jù)的掃描操作。這類(lèi)操作需要訪(fǎng)問(wèn)表中的許多頁(yè),甚至是全部的頁(yè),而這些頁(yè)通常來(lái)說(shuō)又僅在這次查詢(xún)操作中需要,并不是活躍的熱點(diǎn)數(shù)據(jù)。 如果頁(yè)被放入LRU列表的首部,那么非常可能將所需要的熱點(diǎn)數(shù)據(jù)頁(yè)從 LRU列表中移除,而在 下 次需要讀取該頁(yè)時(shí),InnoDB存儲(chǔ)引擎需要再次訪(fǎng)問(wèn)磁盤(pán)。
????????為了解決這個(gè)問(wèn)題,lnnoDB存儲(chǔ)引擎引入了另一個(gè)參數(shù)來(lái)進(jìn)一步管理LRU列表,這個(gè)參數(shù)是innodb _old_ bloks_time,用于表示頁(yè)讀取到Mid位置后需要等待多久才會(huì)被加入到LRU列表的熱端。
? ??????LRU列表用來(lái)管理已經(jīng)讀取的頁(yè), 但當(dāng)數(shù)據(jù)庫(kù)剛啟動(dòng)時(shí),LRU列表是空的, 即沒(méi)有任何的頁(yè)。 這時(shí)頁(yè)都存放在Free列表中。 當(dāng)需要從緩沖池中分頁(yè)時(shí), 首先從Free列表中查找是否有可用的空閑頁(yè), 若有則將該頁(yè)從Free列表中刪除, 放入到LRU列表中。否則, 根據(jù)LRU算法, 淘汰LRU列表末尾的頁(yè),將該內(nèi)存空間分配給新的頁(yè)。 當(dāng)頁(yè)從 LRU列表的old部分加人到new部分 時(shí),稱(chēng)此時(shí)發(fā)生的操作為page made young, 而因?yàn)閕nnodb_old_ blocks_ time的設(shè)置而導(dǎo)致頁(yè)沒(méi)有從old部分移動(dòng)到new部分的操作稱(chēng)為page not made young。 可以通過(guò)命令SHOW ENGINE INNODB STATUS來(lái)觀(guān)察LRU列表及Free列表的使用情況和運(yùn)行狀態(tài)。
? ??????從InnoDB 1.2版本開(kāi)始, 還可以通過(guò)表 INNODB_BUFFER_POOL_STATS來(lái)觀(guān)察緩沖池的運(yùn)行狀態(tài)。此外, 還可以通過(guò)表INNODB_BUFFER_PAGE_LRU來(lái)觀(guān)察每個(gè)LRU列表中每個(gè)頁(yè)的具體信息。
? ??????在LRU列表中的頁(yè)被修改后, 稱(chēng)該頁(yè)為臟頁(yè)(dirty page) , 即緩沖池中的頁(yè)和磁盤(pán)上的頁(yè)的數(shù)據(jù)產(chǎn)生了不一致。 這時(shí)數(shù)據(jù)庫(kù)會(huì)通過(guò)CHECKPOINT機(jī)制將臟頁(yè)刷新回磁盤(pán),而Flush列表中的頁(yè)即為臟頁(yè)列表。需要注意的是,臟頁(yè)既存在于LRU列表中, 也存在于Flush列表中。LRU列表用來(lái)管理緩沖池中頁(yè)的可用性,F(xiàn)lush列表用來(lái)管理將頁(yè)刷新回磁盤(pán),二者互不影響。
? ??????同 LRU列表一樣, Flush列表也可以通過(guò)命令SHOW ENGINE INNODB STATUS 來(lái)查看。
3. 重做日志緩沖
? ?????? InnoDB 存儲(chǔ)引擎的內(nèi)存區(qū)域除了有緩沖池外, 還有重做日志緩沖 (redologbuffer)。 InnoDB 存儲(chǔ)引擎首先將重做日志信息先放入到這個(gè)緩沖區(qū), 然后按一定頻率將其刷新到重做日志文件。 重做日志緩沖一般不需要設(shè)置得很大, 因?yàn)橐话闱闆r下每一秒鐘會(huì)將重做日志緩沖刷新到日志文件, 因此用戶(hù)只需要保證每秒產(chǎn)生的事務(wù)量在這個(gè)緩沖大小之內(nèi)即可。該值可由配置參數(shù)innodb_log_ buffer_ size 控制, 默認(rèn)為8MB。
????????在通常悄況下, 8MB 的重做日志緩沖池足以滿(mǎn)足絕大部分的應(yīng)用, 因?yàn)橹刈鋈罩驹谙铝腥N情況下會(huì)將重做日志緩沖中的內(nèi)容刷新到外部磁盤(pán)的重做日志文件中。
1.Master Thread 每一秒將重做日志緩沖刷新到重做日志文件;
2.每個(gè)事務(wù)提交時(shí)會(huì)將重做日志緩沖刷新到重做日志文件。
3.當(dāng)重做日志緩沖池剩余空間小于1/2 時(shí), 重做日志緩沖刷新到重做日志文件。
4. 額外的內(nèi)存池
? ??????額外的內(nèi)存池通常被DBA 忽略, 他們認(rèn)為該值并不十分重要, 事實(shí)恰恰相反, 該值同樣十分重要。在InnoDB 存儲(chǔ)引擎中, 對(duì)內(nèi)存的管理是通過(guò)一種稱(chēng)為內(nèi)存堆Cheap)的方式進(jìn)行的。在對(duì)一些數(shù)據(jù)結(jié)構(gòu)本身的內(nèi)存進(jìn)行分配時(shí), 需要從額外的內(nèi)存池中進(jìn)行申請(qǐng), 當(dāng)該區(qū)域的內(nèi)存不夠時(shí), 會(huì)從緩沖池中進(jìn)行申請(qǐng)。例如, 分配了緩沖池(innodb_buffer_ pool), 但是每個(gè)緩沖池中的幀緩沖 (framebuffer)還有對(duì)應(yīng)的緩沖控制對(duì)象(buffer control block), 這些對(duì)象記錄了一些諸如LRU、鎖、等待等信息, 而這個(gè)對(duì)象的內(nèi)存需要從額外內(nèi)存池中申請(qǐng)。因此, 在申請(qǐng)了很大的InnoDB 緩沖池時(shí), 也應(yīng)考慮相應(yīng)地增加這個(gè)值。
2.4 Checkpoint 技術(shù)
?????????前面已經(jīng)講到了, 緩沖池的設(shè)計(jì)目的為了協(xié)調(diào)CPU速度與磁盤(pán)速度的鴻溝。 因此頁(yè)的操作首先都是在緩沖池中完成的。 如果一條DML語(yǔ)句, 如Update或Deiete改變了頁(yè) 中的記錄, 那么此時(shí)頁(yè)是臟的, 即緩沖池中的頁(yè)的版本要比磁盤(pán)的新。 數(shù)據(jù)庫(kù)需要將新版本的頁(yè)從緩沖池刷新到磁盤(pán)。
? ??????倘若每次一個(gè)頁(yè)發(fā)生變化, 就將新頁(yè)的版本刷新到磁盤(pán), 那么這個(gè)開(kāi)銷(xiāo)是非常大的。 若熱點(diǎn)數(shù)據(jù)集中在某幾個(gè)頁(yè)中, 那么數(shù)據(jù)庫(kù)的性能將變得非常差。 同時(shí), 如果在從緩沖池將頁(yè)的新版本刷新到磁盤(pán)時(shí)發(fā)生了宥機(jī), 那么數(shù)據(jù)就不能恢復(fù)了。 為了避免發(fā)生數(shù)據(jù)丟失的問(wèn)題, 當(dāng)前事務(wù)數(shù)據(jù)庫(kù)系統(tǒng)普遍都采用了Write Ahead Log策略, 即當(dāng)事務(wù)提交時(shí), 先寫(xiě)重做日志, 再修改頁(yè)。 當(dāng)由于發(fā)生巖機(jī)而導(dǎo)致數(shù)據(jù)丟失時(shí), 通過(guò)重做日志來(lái)完成數(shù)據(jù)的恢復(fù)。 這也是事務(wù) ACID中D (Durability持久性)的要求。
Checkpoint(檢查點(diǎn))技術(shù)的目的是解決以下幾個(gè)問(wèn)題:
1.縮短數(shù)據(jù)庫(kù)的恢復(fù)時(shí)間;
2.緩沖池不夠用時(shí),將臟頁(yè)刷新到磁盤(pán);
3.重做日志不可用時(shí),刷新臟頁(yè)。
? ??????當(dāng)數(shù)據(jù)庫(kù)發(fā)生宕機(jī)時(shí),數(shù)據(jù)庫(kù)不需要重做所有的日志,因?yàn)镃heckpoint之前的頁(yè)都已經(jīng)刷新回磁盤(pán)。故數(shù)據(jù)庫(kù)只需對(duì)Checkpoint后的重做日志進(jìn)行恢復(fù)。這樣就大大縮短了恢復(fù)的時(shí)間。
? ??????此外,當(dāng)緩沖池不夠用時(shí),根據(jù)LRU算法會(huì)溢出最近最少使用的頁(yè),若此頁(yè)為臟頁(yè),那么需要強(qiáng)制執(zhí)行Checkpoint,將臟頁(yè)也就是頁(yè)的新版本刷回磁盤(pán)。
? ??????重做日志出現(xiàn)不可用的情況是因?yàn)楫?dāng)前事務(wù)數(shù)據(jù)庫(kù)系統(tǒng)對(duì)重做日志的設(shè)計(jì)都是循環(huán)使用的,并不是讓其無(wú)限增大的,這從成本及管理上都是比較困難的。重做日志可以被重用的部分是指這些重做日志已經(jīng)不再需要,即當(dāng)數(shù)據(jù)庫(kù)發(fā)生宕機(jī)時(shí),數(shù)據(jù)庫(kù)恢復(fù)操作不需要這部分的重做日志,因此這部分就可以被覆蓋重用。若此時(shí)重做日志還需要使用,那么必須強(qiáng)制產(chǎn)生Checkpoint,將緩沖池中的頁(yè)至少刷新到當(dāng)前重做日志的位置。
? ??????對(duì)于lnnoDB存儲(chǔ)引擎而言,其是通過(guò)LSN(Log Sequence Number)來(lái)標(biāo)記版本的。而LSN是8字節(jié)的數(shù)字,其單位是字節(jié)。每個(gè)頁(yè)有LSN,重做日志中也有LSN, Checkpoint也有LSN。可以通過(guò)命令SHOWENGINE INNODB STATUS來(lái)觀(guān)察。
? ??????Checkpoint所做的事情無(wú)外乎是將緩沖池中的臟頁(yè)刷回到磁盤(pán)。不同之處在于每次刷新多少頁(yè)到磁盤(pán),每次從哪里取臟頁(yè),以及什么時(shí)間觸發(fā)Checkpoint。在InnoDB存儲(chǔ) 引擎內(nèi)部,有兩種Checkpoint,分別為:
1.Sharp Checkpoint
2. Fuzzy Checkpoint
? ??????Sharp Checkpoint發(fā)生在數(shù)據(jù)庫(kù)關(guān)閉時(shí)將所有的臟頁(yè)都刷新回磁盤(pán),這是默認(rèn)的工作方式,即參數(shù)innodb_fast_ shutdown=1。
? ??????是若數(shù)據(jù)庫(kù)在運(yùn)行時(shí)也使用 SharpCheckpoint, 那么數(shù)據(jù)庫(kù)的可用性就會(huì)受到很大的影響。故在InnoDB存儲(chǔ)引擎內(nèi)部使用FuzzyCheckpoint進(jìn)行頁(yè)的刷新,即只刷新一 部分臟頁(yè),而不是刷新所有的臟頁(yè)回磁盤(pán)。在InnoDB存儲(chǔ)引擎中可能發(fā)生如下幾種情況的Fuzzy Checkpoint:
1 Master Thread Checkpoint
2 FLUSH_LRU_LIST Checkpoint
3 Async/Sync Flush Checkpoint
4 Dirty Page too much Checkpoint
? ??????對(duì)于Master Thread (2.5 節(jié)會(huì)詳細(xì)介紹各個(gè)版本中Master Thread的實(shí)現(xiàn))中發(fā)生的Checkpoint, 差不多以每秒或每十秒的速度從緩沖池的臟頁(yè)列表中刷新一定比例的頁(yè)回 磁盤(pán)。這個(gè)過(guò)程是異步的,即此時(shí)InnoDB存儲(chǔ)引擎可以進(jìn)行其他的操作,用戶(hù)查詢(xún)線(xiàn)程不會(huì)阻塞。
????????FLUSH_ LRU _LIST Checkpoint是因?yàn)镮nnoDB存儲(chǔ)引擎需要保證LRU列表中需要有差不多100個(gè)空閑頁(yè)可供使用。在InnoDB1.1.x版本之前,需要檢查L(zhǎng)RU列表中是否有足夠的可用空間操作發(fā)生在用戶(hù)查詢(xún)線(xiàn)程中,顯然這會(huì)阻塞用戶(hù)的查詢(xún)操作。倘若沒(méi)有100個(gè)可用空閑頁(yè),那么InnoDB存儲(chǔ)引擎會(huì)將 LRU列表尾端的頁(yè)移除。如果這些頁(yè)中有臟頁(yè),那么需要進(jìn)行Checkpoint, 而這些頁(yè)是來(lái)自L(fǎng)RU列表的,因此稱(chēng)為FLUSH_ LRU _LIST Checkpoint。
????????而從MySQL 5.6版本,也就是lnnoDB1.2.x版本開(kāi)始,這個(gè)檢查被放在了一個(gè)單獨(dú)的PageCleaner線(xiàn)程中進(jìn)行,并且用戶(hù)可以通過(guò)參數(shù)innodb _ lru _scan_ depth控制LRU列表中可用頁(yè)的數(shù)量,該值默認(rèn)為1024。
? ??????Async/Sync Flush Checkpoint指的是重做日志文件不可用的情況, 這時(shí)需要強(qiáng)制將一些頁(yè)刷新回磁盤(pán), 而此時(shí)臟頁(yè)是從臟頁(yè)列表中選取的 。Async/SyncFlush Checkpoint是為了保證重做日志的循環(huán)使用的可用性。 從lnnoDB1.2.x版本開(kāi)始 也就是MySQL 5.6版本, 這部分的刷新操作同樣放入到了單獨(dú)的PageCleaner線(xiàn)程中進(jìn)行。
? ??????最后一種Checkpoint的情況是Dirty Page too much, 即臟頁(yè)的數(shù)量太多, 導(dǎo)致lnnoDB存儲(chǔ)引擎強(qiáng)制進(jìn)行Checkpoint。 其目的總的來(lái)說(shuō)還是為了保證緩沖池中有足夠可用的頁(yè) 。 其可由參數(shù)innodb_max_ dirty _pages _pct控制。
? ??????innodb _max_ dirty _pages _pct值為75表示, 當(dāng)緩沖池中臟頁(yè)的數(shù)量占據(jù)75%時(shí),強(qiáng)制進(jìn)行Checkpoint, 刷新一部分的臟頁(yè)到磁盤(pán)。 在lnnoDB 1.0.x版本之前, 該參數(shù)默認(rèn)值為90, 之后的版本都為75。
2.5 Master Thread 工作方式
????????在2.3節(jié)中我們知道了,InnoDB存儲(chǔ)引擎的主要工作都是在一個(gè)單獨(dú)的后臺(tái)線(xiàn)程Master Thread中完成的,這一節(jié)將具體解釋該線(xiàn)程的具體實(shí)現(xiàn)及該線(xiàn)程可能存在的問(wèn)題。
2.5.1 lnnoDB 1.0.x版本之前的Master Thread
? ??????Master Thread具有最高的線(xiàn)程優(yōu)先級(jí)別。 其內(nèi)部由多個(gè)循環(huán)(loop)組成: 主循環(huán)(loop)、 后臺(tái)循環(huán)(backgroup loop)、 刷新循環(huán)(flush loop)、 暫停循環(huán)(suspend loop)。 Master Thread會(huì)根據(jù)數(shù)據(jù)庫(kù)運(yùn)行的狀態(tài)在loop、 background loop、 flush loop和suspendloop中進(jìn)行切換。
? ??????Loop被稱(chēng)為主循環(huán),因?yàn)榇蠖鄶?shù)的操作是在這個(gè)循環(huán)中,其中有兩大部分的操作-每秒鐘的操作和每 10秒的操作。
? ??????可以看到,loop循環(huán)通過(guò)thread sleep來(lái)實(shí)現(xiàn),這意味著所謂的每秒一次或每10秒一次的操作是不精確的。在負(fù)載很大的情況下可能會(huì)有延遲(delay), 只能說(shuō)大概在這個(gè)頻率下。當(dāng)然,InnoDB源代碼 中還通過(guò)了其他的方法來(lái)盡量保證這個(gè)頻率每秒一次的操作包括:
1.日志緩沖刷新到磁盤(pán),即使這個(gè)事務(wù)還沒(méi)有提交(總是);
2.合并插入緩沖(可能);
3.至多刷新100個(gè)InnoDB的緩沖池中的臟頁(yè)到磁盤(pán)(可能);
4.如果當(dāng)前沒(méi)有用戶(hù)活動(dòng),則切換到backgro und loop (可能)。
? ??????即使某個(gè)事務(wù)還沒(méi)有提交,InnoDB存儲(chǔ)引擎仍然每秒會(huì)將重做日志緩沖中的內(nèi)容刷新到重做日志文件。這一點(diǎn)是必須要知道的,因?yàn)檫@可以很好地解釋為什么再大的事務(wù)提交(commit)的時(shí)間也是很短的。
? ??????合并插入緩沖(InsertBuffer)并不是每秒都會(huì)發(fā)生的。InnoDB存儲(chǔ)引擎會(huì)判斷當(dāng)前一秒內(nèi)發(fā)生的IO次數(shù)是否小于5次,如果小于5次,InnoDB認(rèn)為當(dāng)前的IO壓力很小,可以執(zhí)行合并插入緩沖的操作。
? ??????同樣, 刷新100個(gè)臟頁(yè)也不是每秒都會(huì)發(fā)生的。InnoDB存儲(chǔ)引擎通過(guò)判斷當(dāng)前緩沖池中臟頁(yè)的比例(buf_get_ modified_ ratio _pct)是否超過(guò)了配置文件中innodb _max_ dirty _pages _pct 這個(gè)參數(shù)(默認(rèn)為90, 代表90%), 如果超過(guò)了這個(gè)闊值,InnoDB存儲(chǔ)引擎認(rèn)為需要做磁盤(pán)同步的操作, 將100個(gè)臟頁(yè)寫(xiě)人磁盤(pán)中。
接著來(lái)看每10秒的操作, 包括如下內(nèi)容:
1.刷新100個(gè)臟頁(yè)到磁盤(pán)(可能的情況下);
2. 合并至多5個(gè)插入緩沖(總是);
3. 將日志緩沖刷新到磁盤(pán)(總是);
4.刪除無(wú)用的Undo頁(yè)(總是);
5. 刷新100個(gè)或者10個(gè)臟頁(yè)到磁盤(pán)(總是)。
? ??????在以上的過(guò)程中,InnoDB存儲(chǔ)引擎會(huì)先判斷過(guò)去10秒之內(nèi)磁盤(pán)的IO操作是否小于200次, 如果是,InnoDB存儲(chǔ)引擎認(rèn)為當(dāng)前有足夠的磁盤(pán)IO操作能力, 因此將100個(gè)臟頁(yè)刷新到磁盤(pán)。 接著,InnoDB存儲(chǔ)引擎會(huì)合并插入緩沖。 不同于每秒一次操作時(shí)可能發(fā)生的合并插人緩沖操作, 這次的合并插人緩沖操作總會(huì)在這個(gè)階段進(jìn)行。 之后, InnoDB存儲(chǔ)引擎會(huì)再進(jìn)行一次將日志緩沖刷新到磁盤(pán)的操作。 這和每秒一次時(shí)發(fā)生的操作是一樣的。
????????接著InnoDB存儲(chǔ) 引擎會(huì)進(jìn)行 一步執(zhí)行full purge操 作, 即刪除 無(wú)用的Undo 頁(yè)。 對(duì)表進(jìn)行update、 delete這類(lèi)操作時(shí), 原先的行被標(biāo)記為刪除, 但是因?yàn)橐恢滦宰x(consistentread)的關(guān)系, 需要保留這些行版本的信息。 但是在full purge過(guò)程中, InnoDB存儲(chǔ)引擎會(huì)判斷當(dāng)前事務(wù)系統(tǒng)中已被刪除的行是否可以刪除, 比如有時(shí)候可能還有查詢(xún)操作需要讀取之前版本的undo信息, 如果可以刪除,lnnoDB會(huì)立即將其刪除。
? ??????從源代碼中可以發(fā)現(xiàn),InnoDB存儲(chǔ)引擎在執(zhí)行fullpurge操作時(shí),每次最多嘗試回收6個(gè)undo頁(yè)。
????????然后,InnoDB存儲(chǔ)引擎會(huì)判斷緩沖池中臟頁(yè)的比例Cbuf_get_modified_ratio_pct),如果有超過(guò)70%的臟頁(yè),則刷新100個(gè)臟頁(yè)到磁盤(pán),如果臟頁(yè)的比例小于70%,則只需刷新10%的臟頁(yè)到磁盤(pán)。
? ? ? ??接著來(lái)看backgroundloop, 若當(dāng)前沒(méi)有用戶(hù)活動(dòng)(數(shù)據(jù)庫(kù)空閑時(shí))或者數(shù)據(jù)庫(kù)關(guān)閉(shutdown), 就會(huì)切換到這個(gè)循環(huán)。backgroundloop會(huì)執(zhí)行以下操作:
1 刪除無(wú)用的Undo頁(yè)(總是);
2 合并20個(gè)插人緩沖(總是);
3 跳回到主循環(huán)(總是);
4.不斷刷新100個(gè)頁(yè)直到符合條件(可能,跳轉(zhuǎn)到flushloop中完成)。
????????若flushloop中也沒(méi)有什么事情可以做了,InnoDB存儲(chǔ)引擎會(huì)切換到suspendloop, 將MasterThread掛起,等待事件的發(fā)生。若用戶(hù)啟用(enable)了InnoDB存儲(chǔ) 引擎,卻沒(méi)有使用任何InnoDB存儲(chǔ)引擎的表,那么MasterThread總是處于掛起的狀態(tài)。
2.5.2 lnnoDB1 .2.x版本之前的Master Thread
? ??????無(wú)論何時(shí),InnoDB存儲(chǔ)引擎最大只會(huì)刷新100個(gè)臟頁(yè)到磁盤(pán), 合并20個(gè)插入緩沖。 如果是在寫(xiě)入密集的應(yīng)用 程序中,每秒可能會(huì)產(chǎn)生大于100個(gè)的臟頁(yè) ,如果是產(chǎn)生大于20個(gè)插入緩沖的情況,Master Thread似乎會(huì) “ 忙不過(guò)來(lái)"'或者說(shuō)它總是做得很慢。即使磁盤(pán)能 在1秒內(nèi)處理多于100個(gè)頁(yè)的寫(xiě)入和20個(gè)插入緩沖的合并,但是由于hard coding, Master Thread也只會(huì)選擇刷新100個(gè)臟頁(yè) 和合并20個(gè)插入緩沖。同時(shí),當(dāng)發(fā)生右機(jī)需要恢復(fù)時(shí),由于很多數(shù)據(jù)還沒(méi)有刷新回磁盤(pán),會(huì)導(dǎo)致恢復(fù)的時(shí)間可能需要很久,尤其是對(duì)于insert buff er來(lái)說(shuō)。
????????InnoDB存儲(chǔ)引擎的開(kāi)發(fā)團(tuán)隊(duì)參考了Google的patch, 提 供了類(lèi)似的方法來(lái)修正該問(wèn)題。因此lnnoDB Plugin (從lnnoDB1.0.x版本開(kāi)始)提供了參數(shù)innodb_ io _capacity, 用來(lái)表示磁盤(pán)IO的吞吐量, 默認(rèn)值 為200。 對(duì)于刷新到磁盤(pán)頁(yè)的數(shù)量,會(huì)按照innodb_io _ capacity 的百分比來(lái)進(jìn)行控制。 規(guī)則如下:
1.在合并插入緩沖時(shí), 合并插入緩沖的數(shù)世為innodb_ io _ capacity值的5%;
2.在從緩沖區(qū)刷新臟頁(yè)時(shí),刷新臟頁(yè)的數(shù)最為innodb_ io _ capacity。
????????另一個(gè)問(wèn)題是,參數(shù)innodb_max_ dirty pages _pct 默認(rèn)值的問(wèn)題, 在InnoDB 1.0. x版本之前, 該值的默認(rèn)為90, 意味著臟頁(yè)占緩沖池的90%。但是該值“ 太大” 了,因?yàn)镮nnoDB存儲(chǔ)引擎在每秒刷新緩沖池和flush loop時(shí)會(huì)判斷這個(gè)值,如果該值大于innodb _max_ dirty _pages _pct, 才刷新100個(gè)臟頁(yè),如果有很大的內(nèi)存, 或者數(shù)據(jù)庫(kù)服務(wù)器的壓力很大,這時(shí)刷新臟頁(yè)的速度反而會(huì)降低。 同樣, 在數(shù)據(jù)庫(kù)的恢復(fù)階段可能需要更多的時(shí)間。
? ??????而從InnoDB1.0.x版本開(kāi)始,innodb_max_ dirty _pages _pct默認(rèn)值變?yōu)榱?5, 和Google測(cè)試的80比較接近。 這樣既可以加快刷新臟頁(yè)的頻率, 又能保證了磁盤(pán)IO的負(fù)載。
? ??????InnoDB 1.0.x版本帶來(lái)的另一個(gè)參數(shù)是 innodb_adaptive_ flushing (自適應(yīng)地刷新), 該值影響每秒刷新臟頁(yè)的數(shù)批。 原來(lái)的刷新規(guī)則是: 臟頁(yè)在緩沖池所占的比例小于 innodb _max_ dirty _pages _pct時(shí), 不刷新臟頁(yè); 大于innodb_max_ dirty _pages _pct時(shí), 刷新100個(gè)臟頁(yè)。 隨著innodb_adaptive_ flushing參數(shù)的引入,InnoDB存儲(chǔ)引擎會(huì)通過(guò)一個(gè)名為buf_ flush _get_ desired_ flush _rate的函數(shù)來(lái)判斷需要刷新臟頁(yè)最合適的數(shù)量。 粗略地翻閱源代碼后發(fā)現(xiàn)buf_ flush _get_ desired_ flush _rate通過(guò)判斷產(chǎn)生重做日志Credo log)的速度來(lái)決定最合適的刷新臟頁(yè)數(shù)拯。 因此, 當(dāng)臟頁(yè)的比例小于innodb_max_ dirty _pages_ pct時(shí), 也會(huì)刷新一定量的臟頁(yè)。
????????還有一個(gè)改變是: 之前每次進(jìn)行full purge操作時(shí),最多回收20個(gè)Undo頁(yè), 從 InnoDB 1.0.x版本開(kāi)始引入了參數(shù) innodb_purge_ batch_ size, 該參數(shù)可以控制每次full purge回收的Undo頁(yè)的數(shù)妞。 該參數(shù)的默認(rèn)值為20, 并可以動(dòng)態(tài)地對(duì)其進(jìn)行修改。
2.5.3 lnnoDB1 .2.x版本的Master Thread
????????在InnoDB1.2.x版本中再次對(duì)MasterThread進(jìn)行了優(yōu)化,由此也可以看出MasterThread對(duì)性能所起到的關(guān)鍵作用。對(duì)于刷新臟頁(yè)的操作,從MasterThread 線(xiàn)程分離到一個(gè)單獨(dú)的PageCleaner Thread, 從而減輕了MasterThread的工作,同時(shí)進(jìn)一步提高了系統(tǒng)的并發(fā)性。
2.6 lnnoDB關(guān)鍵特性
InnoDB存儲(chǔ)引擎的關(guān)鍵特性包括:
1.插入緩沖(InsertBuffer)
2.兩次寫(xiě)(DoubleWrite)
3.自適應(yīng)哈希索引(AdaptiveHash Index)
4.異步IO(Async IO)
5.刷新鄰接頁(yè)(FlushNeighbor Page)
2.6.1 插入緩沖
1. Insert Buffer
? ??????Insert Buffer 可能是 InnoDB 存儲(chǔ)引擎關(guān)鍵特性中最令人激動(dòng)與興奮的一個(gè)功能。 不過(guò)這個(gè)名字可能會(huì)讓人認(rèn)為插入緩沖是緩沖池中的一個(gè)組成部分。 其實(shí)不然, lnnoDB 緩 沖池中有 Insert Buffer 信息固然不錯(cuò), 但是 Insert Buffer 和數(shù)據(jù)頁(yè)一樣, 也是物理頁(yè)的一個(gè)組成部分。
????????在 InnoDB 存儲(chǔ)引擎中, 主鍵是行唯一的標(biāo)識(shí)符。 通常應(yīng)用程序中行記錄的插入順序是按照主鍵遞增的順序進(jìn)行。因此,插入聚集索引一般是順序的,不需要磁盤(pán)的隨機(jī)讀取。
? ??????但是不可能每張表上只有一個(gè)聚集索引, 更多情況下, 一張表上有多個(gè)非聚集的輔助索引 (secondary index)。 比如, 用戶(hù)需要按照 b 這個(gè)字段進(jìn)行查找, 并且 b 這個(gè)字段不是唯一的, 即表是按如下的 SQL 語(yǔ)句定義的:
????????在這樣的情況下產(chǎn)生了了一個(gè)非聚集的且不是唯一的索引。在進(jìn)行插入操作時(shí), 數(shù)據(jù)頁(yè)的存放還是按主鍵a進(jìn)行順序存放的,但是對(duì)于非聚集索引葉子節(jié)點(diǎn)的插入不再是有序的了,這時(shí)就需要離散地訪(fǎng)問(wèn)非聚集索引頁(yè),由于隨機(jī)讀取的存在而導(dǎo)致了插入操作性能下降。當(dāng)然這并不是索引這個(gè)B字段上索引的錯(cuò)誤,而是因?yàn)锽+樹(shù)的特性決定了非聚集索引插入的離散性。
????????需要注意的是,在某些情況下,輔助索引的插人依然是順序的,或者說(shuō)是比較順序的,比如用戶(hù)購(gòu)買(mǎi)表中的時(shí)間字段。在通常情況下,用戶(hù)購(gòu)買(mǎi)時(shí)間是一個(gè)輔助索引,用來(lái)根據(jù)時(shí)間條件 進(jìn)行查詢(xún)。但是在插入時(shí)根據(jù)時(shí)間的遞增而插入的,因此插入也是“較為”順序的。
? ??????InnoDB 存儲(chǔ)引擎開(kāi)創(chuàng)性地設(shè)計(jì)了 Insert Buffer, 對(duì)于非聚集索引的插入或更新操作,不是每一次直接插入到索引頁(yè)中,而是先判斷插入的非聚集索引頁(yè)是否在緩沖池中,若在,則直接插入;若不在,則先放入到一個(gè) Insert Buffer 對(duì)象中,好似欺騙數(shù)據(jù)庫(kù)這個(gè)非聚集的索引已經(jīng)插到葉子節(jié)點(diǎn),而實(shí)際并沒(méi)有,只是存放在另 一個(gè)位置。然后再以一定的頻率和悄況進(jìn)行Insert Buffer 和輔助索引頁(yè)子節(jié)點(diǎn)的merge (合并)操作,這時(shí)通常能將多個(gè)插入合并到一個(gè)操作中(因?yàn)樵谝粋€(gè)索引頁(yè)中),這就大大提高了對(duì)于非聚集索引插入的性能。
????????然而Insert Buffer 的使用需要同時(shí)滿(mǎn)足以下兩個(gè)條件:
1.索引是輔助索引(secondary index);
2.索引不是唯一(unique) 的。
????????當(dāng)滿(mǎn)足以上兩個(gè)條件時(shí),InnoDB 存儲(chǔ)引擎會(huì)使用Insert Buffer, 這樣就能提高插入操作的性能了。不過(guò)考慮這樣一種情況:應(yīng)用程序進(jìn)行大量的插入操作,這些都涉及了不唯一的非聚集索引,也就是使用了Insert Buffer。若此時(shí)MySQL 數(shù)據(jù)庫(kù)發(fā)生了宕機(jī),這時(shí)勢(shì)必有大掀的Insert Buffer 并沒(méi)有合并到實(shí)際的非聚集索引中去。因此這時(shí)恢復(fù)可能需要很長(zhǎng)的時(shí)間,在極端情況下甚至需要幾個(gè)小時(shí)。
????????輔助索引不能是唯一的,因?yàn)樵诓迦刖彌_時(shí),數(shù)據(jù)庫(kù)并不去查找索引頁(yè)來(lái)判斷插入的記錄的唯一性。如果去查找肯定又會(huì)有離散讀取的情況發(fā)生,從而導(dǎo)致Insert Buffer失去了意義。
????????用戶(hù)可以通過(guò)命令SHOW ENGINE INNODB STATUS 來(lái)查看插入緩沖的信息
2. Change Buffer
? ??????InnoDB從1.0.x版本開(kāi)始引入了 Change Buffer, 可將其視為Insert Buffer的升級(jí)。從這個(gè)版本開(kāi)始,lnnoDB存儲(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è)過(guò)程:
1.將記錄標(biāo)記為己刪除;
2.真正將記錄刪除。
? ??????因此Delete Buffer對(duì)應(yīng)UPDATE操作的第一個(gè)過(guò)程,即將記錄標(biāo)記為刪除。PurgeBuffer對(duì)應(yīng)UPDATE操作的第二個(gè)過(guò)程,即將記錄真正的 刪除。同時(shí),lnnoDB存儲(chǔ)引 擎提供了參數(shù)innodb_change_ buffering, 用來(lái)開(kāi)啟各種Buffer的選項(xiàng)。該參數(shù)可選的值為: inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面討論過(guò)的三種情況。changes表示啟用 inserts和deletes, all表示啟用所有,none表示都不啟用 。該參數(shù)默認(rèn)值為all。
????????從InnoDB 1.2.x版本開(kāi)始,可以通過(guò)參數(shù)innodb_change_ buff er_ max_ size來(lái)控制 Change Buffer最大使用內(nèi)存的數(shù)量。
? ??????innodb _change_ buffer_ max_ size值 默認(rèn)為25, 表示最多使用1/4的緩沖池內(nèi)存空間。而需要注意的是,該參數(shù)的最大有效值為50。
3. Insert Buffer 的內(nèi)部實(shí)現(xiàn)
????????通過(guò)前一個(gè)小節(jié)讀者應(yīng)該已經(jīng)知道了 Insert Buffer 的使用場(chǎng)景, 即非唯一輔助索引的插入操作。 但是對(duì)于 Insert Buffer 具體是什么, 以及內(nèi)部怎么實(shí)現(xiàn)可能依然模糊, 這 正是本節(jié)所要闡述的內(nèi)容。
????????可能令絕大部分用戶(hù)感到吃驚的是, Insert Buffer 的數(shù)據(jù)結(jié)構(gòu)是一棵 B+ 樹(shù)。 在MySQL 4.1 之前的版本中每張表有一棵 InsertBuffer B+ 樹(shù)。 而在現(xiàn)在的版本中, 全局只有一棵 Insert Buffer B+ 樹(shù), 負(fù)責(zé)對(duì)所有的表的輔助索引進(jìn)行 Insert Buffer。 而這棵 B+ 樹(shù)存放在共享表空間中, 默認(rèn)也就是 ibdatal 中。 因此, 試圖通過(guò)獨(dú)立表空間 ibd 文件恢復(fù)表中數(shù)據(jù)時(shí), 往往會(huì)導(dǎo)致 CHECK TABLE 失敗。 這是因?yàn)楸淼妮o助索引中的數(shù)據(jù)可能還在 Insert Buffer 中, 也就是共享表空間中, 所以通過(guò) ibd 文件進(jìn)行恢復(fù)后, 還需要進(jìn)行REPAIR TABLE操作來(lái)重建表上所有的輔助索引。
????????Insert Buffer是一棵 B+ 樹(shù), 因此其也由葉節(jié)點(diǎn)和非葉節(jié)點(diǎn)組成。 非葉節(jié)點(diǎn)存放的是查詢(xún)的 search key (鍵值), 其構(gòu)造如圖 2-3 所示。
? ? ? ? ?search key 一共占用9 個(gè)字節(jié), 其中 space 表示待插入記錄所在表的表空間 id, 在 lnnoDB 存儲(chǔ)引擎中, 每個(gè)表有一個(gè)唯一的 space id, 可以通過(guò) space id 查詢(xún)得知是哪張表。 space 占用 4 字節(jié)。 marker 占用1 字節(jié), 它是用來(lái)兼容老版本的 Insert Buffer。 offset 表示頁(yè)所在的偏移抵, 占用4 字節(jié)。
? ??????當(dāng)一個(gè)輔助索引要插入到頁(yè)(space,offset)時(shí),如果這個(gè)頁(yè)不在緩存池中,那lnnoDB存儲(chǔ)引擎首先根據(jù)上述規(guī)則構(gòu)造一個(gè)searchkey, 接下來(lái)查詢(xún)InsertBuffer這棵B+樹(shù),然后再將這條記錄插人到InsertBuffer B+樹(shù)的葉子節(jié)點(diǎn)中。
? ??????對(duì)于插入到 Insert Buffer B+ 樹(shù)葉子節(jié)點(diǎn)的記錄(如圖 2-4 所示),并不是直接將待插入的記錄插入,而是需要根據(jù)如下的規(guī)則進(jìn)行構(gòu)造:
????????space、 marker、 page_no 字段和之前非葉節(jié)點(diǎn)中的含義相同,一共占用9字節(jié)。第4 個(gè)字段 metadata 占用4 字節(jié),其存儲(chǔ)的內(nèi)容如表 2-2 所示。
? ??????IBUF _REC_OFFSET_COUNT是保存兩個(gè)字節(jié)的整數(shù),用來(lái)排序每個(gè)記錄進(jìn)入 Insert Buffer的順序。因?yàn)閺腎nnoDBl.0.x開(kāi)始支持ChangeBuffer, 所以這個(gè)值同樣記錄進(jìn)入InsertBuffer的順序。通過(guò)這個(gè)順序回放(replay)才能得到記錄的正確值。
????????從InsertBuffer葉子節(jié)點(diǎn)的第5列開(kāi)始,就是實(shí)際插入記錄的各個(gè)字段了。因此較之原插入記錄,InsertBuffer B+樹(shù)的葉子節(jié)點(diǎn)記錄需要額外13字節(jié)的開(kāi)銷(xiāo)。
????????因?yàn)閱⒂?Insert Buffer 索引后,輔助索引頁(yè)C space, page_ no) 中的記錄可能被插入到 Insert Buffer B+ 樹(shù)中,所以為了保證每次 Merge Insert Buffer 頁(yè)必須成功,還需要有一個(gè)特殊的頁(yè)用來(lái)標(biāo)記每個(gè)輔助索引頁(yè)的可用空間。這個(gè)頁(yè)的類(lèi)型為InsertBufferBitmap.
? ??????每個(gè)InsertBuffer Bitmap頁(yè)用來(lái)追蹤16384個(gè)輔助索引頁(yè),也就是256個(gè)區(qū) (Extent)。每個(gè)InsertBuffer Bitmap頁(yè)都在16384個(gè)頁(yè)的第二個(gè)頁(yè)中。關(guān)于InsertBuff er Bitmap頁(yè)的作用會(huì)在下一小節(jié)中詳細(xì)介紹。
? ??????每個(gè)輔助索引頁(yè)在InsertBuffer Bitmap頁(yè)中占用4位(bit),由表2-3中的三個(gè)部分組成。
4. Merge Insert Buffer
? ??????通過(guò)前面的小節(jié)讀者應(yīng)該已經(jīng)知道了Insert/Change Buffer是一棵B+樹(shù)。 若需要實(shí)現(xiàn)插入記錄的輔助索引頁(yè)不在緩沖池中, 那么需要將輔助索引記錄首先插入到這棵B+ 樹(shù)中。 但是Insert Buffer中的記錄何時(shí)合并(merge)到真正的輔助索引中呢?這是本小節(jié)需要關(guān)注的重點(diǎn)。
概括地說(shuō), MergeInsert Buffer的操作可能發(fā)生在以下幾種情況下:
1 輔助索引頁(yè)被讀取到緩沖池時(shí):
2 Insert Buffer Bitmap頁(yè)追蹤到該輔助索引頁(yè)已無(wú)可用空間時(shí);
3 Master Thread。
? ??????第一種情況為當(dāng)輔助索引頁(yè)被讀取到緩沖池中時(shí), 例如這在執(zhí)行正常的SELECT查詢(xún)操作, 這時(shí)需要檢查InsertBuffer Bitmap頁(yè), 然后確認(rèn)該輔助索引頁(yè)是否有記錄存放于InsertBuffer B+樹(shù)中。 若有, 則將Insert Buffer B+樹(shù)中該頁(yè)的記錄插入到該輔助索引 頁(yè)中。 可以看到對(duì)該頁(yè)多次的記錄操作通過(guò)一次操作合并到了原有的輔助索引頁(yè)中, 因此性能會(huì)有大幅提高。
? ? ? ? Insert Buffer Bitmap頁(yè)用來(lái)追蹤每個(gè)輔助索引頁(yè)的可用空間, 并至少有1/32頁(yè)的空間。 若插入輔助索引記錄時(shí)檢測(cè)到插人記錄后可用空間會(huì)小于1/32頁(yè), 則會(huì)強(qiáng)制進(jìn)行一個(gè)合并操作, 即強(qiáng)制讀取輔助索引頁(yè), 將Insert Buffer B+樹(shù)中該頁(yè)的記錄及待插入的記錄插人到輔助索引頁(yè)中。 這就是上述所說(shuō)的第二種情況。
????????還有一種情況, 之前在分析Master Thread 時(shí)曾講到, 在Master Thread線(xiàn)程中每秒或每10秒會(huì)進(jìn)行一次MergeInsert Buffer的操作, 不同之處在千每次進(jìn)行merge操作的頁(yè)的數(shù)量不同。
? ??????在MasterThread中,執(zhí)行merge操作的不止是一個(gè)頁(yè),而是根據(jù)srv_innodb_io_capacity的百分比來(lái)決定真正要合并多少個(gè)附注索引頁(yè)。但I(xiàn)nnoDB存儲(chǔ)引擎又是根據(jù)怎樣的算法來(lái)得知需要合并的輔助索引頁(yè)呢?
? ??????在Insert Buffer B+樹(shù)中, 輔助索引頁(yè)根據(jù)(space, offset)都已排序好, 故可以根據(jù)(space, offset) 的排序順序進(jìn)行頁(yè)的選擇。 然而, 對(duì)于Insert Buffer頁(yè)的選擇, lnnoDB存儲(chǔ)引擎并非采用這個(gè)方式, 它隨機(jī)地選擇Insert Buffer B+樹(shù)的一個(gè)頁(yè), 讀取該頁(yè)中的 space及之后所需要數(shù)扯的頁(yè)。 該算法在復(fù)雜情況下應(yīng)有更好的公平性。 同時(shí), 若進(jìn)行merge時(shí), 要進(jìn)行merge的表已經(jīng)被刪除, 此時(shí)可以直接丟棄已經(jīng)被Insert/Change Buffer的數(shù)據(jù)記錄。
2.6.2 兩次寫(xiě)
? ??????如果說(shuō)Insert Buffer帶給lnnoDB存儲(chǔ)引擎的是性能上的提升, 那么doublewrite(兩 次寫(xiě))帶給InnoDB存儲(chǔ)引擎的是數(shù)據(jù)頁(yè)的可靠性。
????????當(dāng)發(fā)生數(shù)據(jù)庫(kù)宕機(jī)時(shí), 可能lnnoDB存儲(chǔ)引擎正在寫(xiě)入某個(gè)頁(yè)到表中, 而這個(gè)頁(yè)只寫(xiě)了一部分, 比如16KB 的頁(yè), 只寫(xiě)了前4KB, 之后就發(fā)生了巖機(jī), 這種情況被稱(chēng)為部分寫(xiě)失效(partial page write)。 在InnoDB存儲(chǔ)引擎未使用doublewrite技術(shù)前, 曾經(jīng)出現(xiàn)過(guò)因?yàn)椴糠謱?xiě)失效而導(dǎo)致數(shù)據(jù)丟失的情況。
? ??????doublewrite由兩部分組成, 一部分是內(nèi)存中的doublewrite buffer, 大小為2MB , 另一部分是物理磁盤(pán)上共享表空間中連續(xù)的128個(gè)頁(yè), 即2個(gè)區(qū)(extent) , 大小同樣為 2MB。 在對(duì)緩沖池的臟頁(yè)進(jìn)行刷新時(shí), 并不直接寫(xiě)磁盤(pán), 而是會(huì)通過(guò) memcpy函數(shù)將臟頁(yè)先復(fù)制到內(nèi)存中的doublewritebuffer, 之后通過(guò)doublewrite buffer再分兩次, 每次 1MB 順序地寫(xiě)入共享表空間的物理磁盤(pán)上, 然后馬上調(diào)用fsync函數(shù), 同步磁盤(pán), 避免緩沖寫(xiě)帶來(lái)的問(wèn)題。 在這個(gè)過(guò)程中, 因?yàn)閐oublewrite頁(yè)是連續(xù)的, 因此這個(gè)過(guò)程是順序?qū)懙模_(kāi)銷(xiāo)并不是很大。在完成 doublewrite 頁(yè)的寫(xiě)入后,再將 doublewrite buffer 中的頁(yè)寫(xiě)入各個(gè)表空間文件中,此時(shí)的寫(xiě)入則是離散的。可以通過(guò)以下命令觀(guān)察到 doublewrite 運(yùn)行的情況
2.6.3 自適應(yīng)哈希索引
? ??????哈希(hash) 是一種非常快的查找方法, 在一般情況下這種查找的時(shí)間復(fù)雜度為O(1), 即一般僅需要一次查找就能定位數(shù)據(jù)。而B(niǎo)+樹(shù)的查找次數(shù), 取決于B+樹(shù)的高度, 在生產(chǎn)環(huán)境中, B+樹(shù)的高度一般為3-4層, 故需要3-4次的查詢(xún)。
????????InnoDB存儲(chǔ)引擎會(huì)監(jiān)控對(duì)表上各索引頁(yè)的查詢(xún)。如果觀(guān)察到建立哈希索引可以帶來(lái)速度提升, 則建立哈希索引, 稱(chēng)之為自適應(yīng)哈希索引(A daptiveH ash Index, AHi)。AHI是通過(guò)緩沖池的B+樹(shù)頁(yè)構(gòu)造而來(lái), 因此建立的速度很快, 而且不需要對(duì)整張表構(gòu)建哈希索引。InnoDB存儲(chǔ)引擎會(huì)自動(dòng)根據(jù)訪(fǎng)問(wèn)的頻率和模式來(lái)自動(dòng)地為某些熱點(diǎn)頁(yè)建立哈希索引。
????????AHI有一個(gè)要求, 即對(duì)這個(gè)頁(yè)的連續(xù)訪(fǎng)問(wèn)模式必須是一樣的。例如對(duì)于(a, b) 這樣的聯(lián)合索引頁(yè),其訪(fǎng)問(wèn)模式可以是以下情況:
WHERE a=xxx
WHERE a=xxx and b=xxx
? ??????訪(fǎng)問(wèn)模式一樣指的是查詢(xún)的條件一樣,若交替進(jìn)行上述兩種查詢(xún),那么InonDB存 儲(chǔ)引擎不會(huì)對(duì)該頁(yè)構(gòu)造AHi。此外AHi還有如下的要求:
1.以該模式訪(fǎng)問(wèn)了100次
2.頁(yè)通過(guò)該模式訪(fǎng)問(wèn)了N次,其中N=頁(yè)中記錄*1/16
????????根據(jù)lnnoDB存儲(chǔ)引擎官方的文檔顯示,啟用AHi后,讀取和寫(xiě)入速度可以提高2倍,輔助索引的連接操作性能可以提高5倍。毫無(wú)疑問(wèn),AHi是非常好的優(yōu)化模式,其設(shè)計(jì)思想是數(shù)據(jù)庫(kù)自?xún)?yōu)化的(self-tuning),即無(wú)需OBA對(duì)數(shù)據(jù)庫(kù)進(jìn)行人為調(diào)整。通過(guò)命令SHOWENGINE INNODB STATUS可以看到當(dāng)前AHi的使用狀況。
2.6.4 異步IO
????????為了提高磁盤(pán)操作性能, 當(dāng)前的數(shù)據(jù)庫(kù) 系統(tǒng)都采用異步IO (Asynchronous IO, AIO) 的方式來(lái)處理磁盤(pán)操作。lnnoDB存儲(chǔ)引擎亦是如此 。
????????與AIO對(duì)應(yīng)的是 Sync IO, 即每進(jìn)行一次IO操作, 需要等待此次操作結(jié)束才能繼續(xù)接下來(lái)的操作。 但是如果用戶(hù) 發(fā)出的是一條索引掃描的查詢(xún), 那么這條SQL 查詢(xún)語(yǔ)句可能需要掃描多個(gè)索引頁(yè) ,也就是需要進(jìn)行多次的IO操作。 在每掃描一個(gè)頁(yè)并等待其完成后再進(jìn)行下一次的掃描 ,這是沒(méi)有必要的 。用戶(hù)可以在 發(fā)出一個(gè)IO請(qǐng)求后立即再 發(fā)出另一個(gè)IO請(qǐng)求, 當(dāng)全部IO請(qǐng)求發(fā)送完畢后, 等待所有IO操作的完成, 這就是AIO。
????????AIO的另一個(gè)優(yōu)勢(shì)是可以進(jìn)行IO Merge操作, 也就是將多個(gè)IO合并為1個(gè)IO, 這樣可以提高IOPS的性能。例如用戶(hù)需要訪(fǎng)問(wèn)頁(yè)的(space, page_ no)為:(8, 6)、(8, 7), (8, 8)每個(gè)頁(yè)的大小為 16KB, 那么同步IO需要進(jìn)行3次IO操作。 而AIO會(huì)判斷到這三個(gè)頁(yè)是連續(xù)的(顯然可以通過(guò)(space, page_ no)得知)。 因此AIO底層會(huì)發(fā)送一個(gè)IO請(qǐng)求, 從(8, 6) 開(kāi)始 , 讀取48KB的頁(yè) 。
2.6.5 刷新鄰接頁(yè)
? ??????InnoDB存儲(chǔ)引擎還提供了Flush Neighbor Page (刷新鄰接頁(yè))的特性。其工作原理為:當(dāng)刷新一個(gè)臟頁(yè)時(shí),InnoDB存儲(chǔ)引擎會(huì)檢測(cè)該頁(yè)所在區(qū)( extent)的所有頁(yè),如果是臟頁(yè),那么一起進(jìn)行刷新。這樣做的好處顯而易見(jiàn),通過(guò)AIO可以將多個(gè)IO寫(xiě)入操作合并為一個(gè)IO操作,故該工作機(jī)制在傳統(tǒng)機(jī)械磁盤(pán)下有著顯著的優(yōu)勢(shì)。但是需要考 慮到下面兩個(gè)問(wèn)題:
1.是不是可能將不怎么臟的頁(yè)進(jìn)行 了寫(xiě)入,而 該頁(yè)之后又會(huì)很快變成臟頁(yè)?
2.固態(tài)硬盤(pán)有著較高的IOPS, 是否還需要這個(gè)特性?
2.7 啟動(dòng)、 關(guān)閉與恢復(fù)
? ??????InnoDB是MySQL數(shù)據(jù)庫(kù)的存儲(chǔ)引擎之一,因此InnoDB存儲(chǔ)引擎的啟動(dòng)和關(guān)閉, 更準(zhǔn)確的是指在MySQL實(shí)例的啟動(dòng)過(guò)程中對(duì)lnnoDB存儲(chǔ)引擎的處理過(guò)程。
? ??????在關(guān)閉時(shí),參數(shù)innodb_ fast_ shutdown影響著表的存儲(chǔ)引擎為lnnoDB的4行為,該參數(shù)可取值為0、1、2,默認(rèn)值為1。
1.0表示在MySQL數(shù)據(jù)庫(kù)關(guān)閉時(shí),InnoDB需要完成所有的fullpurge和mergeinsert buffer, 并且將所有的臟頁(yè)刷新回磁盤(pán)。這需要一些時(shí)間,有時(shí)甚至需要幾個(gè)小時(shí)來(lái)完成。如果在進(jìn)行InnoDB升級(jí)時(shí),必須將這個(gè)參數(shù)調(diào)為0,然后再關(guān)閉數(shù)據(jù)庫(kù)。
2.1是參數(shù)innodb_fast_ shutdown的默認(rèn)值,表示不需要完成上述的fullpurge和merge insert buff er操作,但是在緩沖池中的一些數(shù)據(jù)臟頁(yè)還是會(huì)刷新回磁盤(pán)。
3.2表示不完成fullpurge和mergeinsert buffer操作,也不將緩沖池中的數(shù)據(jù)臟頁(yè)寫(xiě)回磁盤(pán),而是將日志都寫(xiě)入日志文件。這樣不會(huì)有任何事務(wù)的丟失,但是下次MySQL數(shù)據(jù)庫(kù)啟動(dòng)時(shí),會(huì)進(jìn)行恢復(fù)操作(recovery)。
????????當(dāng)正常關(guān)閉MySQL數(shù)據(jù)庫(kù)時(shí),下次的啟動(dòng)應(yīng)該會(huì)非常“正常”。但是如果沒(méi)有正常地關(guān)閉數(shù)據(jù)庫(kù),如用kill命令關(guān)閉數(shù)據(jù)庫(kù),在MySQL數(shù)據(jù)庫(kù)運(yùn)行中重啟了服務(wù)器,或者在關(guān)閉數(shù)據(jù)庫(kù)時(shí),將參數(shù)innodb_ fast_ shutdown設(shè)為了2時(shí),下次MySQL數(shù)據(jù)庫(kù)啟動(dòng)時(shí)都會(huì)對(duì)InnoDB存儲(chǔ)引擎的表進(jìn)行恢復(fù)操作。
????????參數(shù)innodb_force_ recovery影響了整個(gè)InnoDB存儲(chǔ)引擎恢復(fù)的狀況。該參數(shù)值默認(rèn)為0,代表當(dāng)發(fā)生需要恢復(fù)時(shí),進(jìn)行所有的恢復(fù)操作,當(dāng)不能進(jìn)行有效恢復(fù)時(shí),如數(shù)據(jù)頁(yè)發(fā)生了corruption,MySQL數(shù)據(jù)庫(kù)可能發(fā)生宥機(jī)(crash),并把錯(cuò)誤寫(xiě)入錯(cuò)誤日志中去。
????????但是,在某些情況下,可能并不需要進(jìn)行完整的恢復(fù)操作,因?yàn)橛脩?hù)自已知道怎么進(jìn)行恢復(fù)。比如在對(duì)一個(gè)表進(jìn)行altertable操作時(shí)發(fā)生意外了,數(shù)據(jù)庫(kù)重啟時(shí)會(huì)對(duì) InnoDB表進(jìn)行回滾操作,對(duì)于一個(gè)大表來(lái)說(shuō)這需要很長(zhǎng)時(shí)間,可能是幾個(gè)小時(shí)。這時(shí)用戶(hù)可以自行進(jìn)行恢復(fù),如可以把表刪除,從備份中重新導(dǎo)入數(shù)據(jù)到表,可能這些操作的速度要遠(yuǎn)遠(yuǎn)快千回滾操作。
????????參數(shù)innodb_ force _recovery還可以設(shè)置為6個(gè)非零值:1~6。大的數(shù)字表示包含了 前面所有小數(shù)字表示的影響。具體情況如下:
1.(SRV _FORCE_IGNORE_CORRUPT): 忽略檢查到的corrupt頁(yè)。
2. 2(SRV _FORCE_NO _BACKGROUND): 阻止MasterThread線(xiàn)程的運(yùn)行,如MasterThread線(xiàn)程需要進(jìn)行fullpurge操作,而這會(huì)導(dǎo)致crash。
3. 3(SRV _FORCE_NO_TRX_UNDO): 不進(jìn)行事務(wù)的回滾操作。
4.(SRV FORCE_NO_IBUF MERGE):不進(jìn)行插入緩沖的合并操作。
5.5(SRV FORCE_NO_UNDO_LOG_SCAN): 不查看撤銷(xiāo)日志(Undo Log), lnnoDB存儲(chǔ)引擎會(huì)將未提交的事務(wù)視為已提交。
6.6(SRV FORCE_NO_LOG_REDO): 不進(jìn)行前滾的操作。
????????需要注意的是, 在設(shè)置了參數(shù)innodb_force_ recovery 大于0 后, 用戶(hù)可以對(duì)表進(jìn)行select 、create 和drop 操作, 但insert 、update 和delete 這類(lèi)DML 操作是不允許的。
2.8 小結(jié)
????????本章對(duì) InnoDB 存儲(chǔ)引擎及其體系結(jié)構(gòu)進(jìn)行了概述, 先給出了 InnoDB 存儲(chǔ)引擎的歷史、 InnoDB 存儲(chǔ)引擎的體系結(jié)構(gòu)(包括后臺(tái)線(xiàn)程和內(nèi)存結(jié)構(gòu)) ; 之后又詳細(xì)介紹了 InnoDB 存儲(chǔ)引擎的關(guān)鍵特性, 這些特性使 InnoDB 存儲(chǔ)引擎變得更具 “魅力 ”; 最后介紹了啟動(dòng)和關(guān)閉 MySQL 時(shí)一些配置文件參數(shù)對(duì) InnoDB 存儲(chǔ)引擎的影響。
????????通過(guò)本章的鋪墊, 讀者在學(xué)習(xí)后面的內(nèi)容時(shí)就會(huì)對(duì) InnoDB 引擎理解得更深入和更全面。 第 3 章開(kāi)始介紹 MySQL 的文件, 包括 MyS'QL 本身的文件和與 InnoDB 存儲(chǔ)引擎本身有關(guān)的文件。 之后本書(shū)將介紹基千 InnoDB 存儲(chǔ)引擎的表, 并揭示內(nèi)部的存儲(chǔ)構(gòu)造。