從Mysql5.5版本開始,InnoDB是默認的表存儲引擎。其特色是行鎖設計、支持MVCC、支持外鍵、提供一致性非鎖定讀、同時被設計用來最有效的利用以及使用內存和CPU。html
下圖簡單描述了InnoDB存儲引擎的體系結構:算法
InnoDB存儲引擎有多個內存塊,這些內存塊組成了一個大的內存池。後臺線程主要負責刷新內存池中的數據、將已修改的數據刷新到磁盤等等。接下來咱們分別介紹後臺線程和內存池。sql
InnoDB後臺有多個不一樣的線程,用來負責不一樣的任務。主要有以下:數據庫
這一部分,我將網上的比較好的資料盡力去綜合,直觀的描述出來。
InnoDB 存儲引擎是基於磁盤存儲的,也就是說數據都是存儲在磁盤上的,因爲 CPU 速度和磁盤速度之間的鴻溝, InnoDB 引擎使用緩衝池技術來提升數據庫的總體性能。緩衝池簡單來講就是一塊內存區域.在數據庫中進行讀取頁的操做,首先將從磁盤讀到的頁存放在緩衝池中,下一次讀取相同的頁時,首先判斷該頁是否是在緩衝池中,若在,稱該頁在緩衝池中被命中,直接讀取該頁。不然,讀取磁盤上的頁。對於數據庫中頁的修改操做,首先修改在緩衝池中頁,而後再以必定的頻率刷新到磁盤,並非每次頁發生改變就刷新回磁盤。緩存
緩衝池中緩存的數據頁類型有:索引頁、數據頁、 undo 頁、插入緩衝、自適應哈希索引、 InnoDB 的鎖信息、數據字典信息等。索引頁和數據頁佔緩衝池的很大一部分(知道有這些頁,把這些頁當作名詞便可,不用感到迷惑)。在InnoDB中,緩衝池中的頁大小默認爲16KB。服務器
咱們已經知道這個Buffer Pool實際上是一片連續的內存空間,那如今就面臨這個問題了:怎麼將磁盤上的頁緩存到內存中的Buffer Pool中呢?直接把須要緩存的頁向Buffer Pool裏一個一個往裏懟麼?不不不,爲了更好的管理這些被緩存的頁,InnoDB爲每個緩存頁都建立了一些所謂的控制信息,這些控制信息包括該頁所屬的表空間編號、頁號、頁在Buffer Pool中的地址,一些鎖信息以及LSN信息(鎖和LSN這裏能夠先忽略),固然還有一些別的控制信息。數據結構
每一個緩存頁對應的控制信息佔用的內存大小是相同的,咱們就把每一個頁對應的控制信息佔用的一塊內存稱爲一個控制塊吧,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 後邊,因此整個Buffer Pool對應的內存空間看起來就是這樣的:架構
控制塊和緩存頁之間的那個碎片是個什麼呢?你想一想啊,每個控制塊都對應一個緩存頁,那在分配足夠多的控制塊和緩存頁後,可能剩餘的那點兒空間不夠一對控制塊和緩存頁的大小,天然就用不到嘍,這個用不到的那點兒內存空間就被稱爲碎片了。固然,若是你把Buffer Pool的大小設置的剛恰好的話,也可能不會產生碎片~異步
前面咱們知道了緩衝池的結構。接下來講InnoDB存儲引擎是怎麼對緩衝池進行管理的。講下我當時的困惑吧,(這部分能夠不看,由於有些名詞還沒涉及到)看《Mysql技術內幕》這本書,講LRU List比較多,Free List只是順帶一過,沒有將free list初始時是怎麼分配的或者是什麼樣的結構,致使我對緩衝池的管理老是想象不出來,後來通過在網上尋找資料,算是弄明白了。來看一下吧:async
從圖中能夠看出,咱們爲了管理好這個Free鏈表,特地爲這個鏈表定義了一個控制信息,裏邊兒包含着鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。咱們在每一個Free鏈表的節點中都記錄了某個緩存頁控制塊的地址,而每一個緩存頁控制塊都記錄着對應的緩存頁地址,因此至關於每一個Free鏈表節點都對應一個空閒的緩存頁。
有了這個Free鏈表事兒就好辦了,每當須要從磁盤中加載一個頁到Buffer Pool中時,就從Free鏈表中取一個空閒的緩存頁,而且把該緩存頁對應的控制塊的信息填上,而後把該緩存頁對應的Free鏈表節點從鏈表中移除,表示該緩存頁已經被使用了~
不要由於走的太遠而忘記爲何出發。
簡單回顧一下,爲何講free list?是爲了講怎麼管理buffer pool對吧。那free list就至關因而數據庫服務剛剛啓動沒有數據頁時,維護buffer pool的空閒緩存頁的數據結構。
下面再來簡單地回顧Buffer Pool的工做機制。Buffer Pool兩個最主要的功能:一個是加速讀,一個是加速寫。加速讀呢? 就是當須要訪問一個數據頁面的時候,若是這個頁面已經在緩存池中,那麼就再也不須要訪問磁盤,直接從緩衝池中就能獲取這個頁面的內容。加速寫呢?就是當須要修改一個頁面的時候,先將這個頁面在緩衝池中進行修改,記下相關的重作日誌,這個頁面的修改就算已經完成了。至於這個被修改的頁面何時真正刷新到磁盤,這個是後臺刷新線程來完成的。
在實現上面兩個功能的同時,須要考慮客觀條件的限制,由於機器的內存大小是有限的,因此MySQL的InnoDB Buffer Pool的大小一樣是有限的,若是須要緩存的頁佔用的內存大小超過了Buffer Pool大小,也就是Free鏈表中已經沒有多餘的空閒緩存頁的時候豈不是很尷尬,發生了這樣的事兒該咋辦?固然是把某些舊的緩存頁從Buffer Pool中移除,而後再把新的頁放進來嘍~ 那麼問題來了,移除哪些緩存頁呢?
爲了回答這個問題,咱們還須要回到咱們設立Buffer Pool的初衷,咱們就是想減小和磁盤的I/O交互,最好每次在訪問某個頁的時候它都已經被緩存到Buffer Pool中了。假設咱們一共訪問了n次頁,那麼被訪問的頁已經在緩存中的次數除以n就是所謂的緩存命中率,咱們的指望就是讓緩存命中率越高越好~
怎麼提升緩存命中率呢?InnoDB Buffer Pool採用經典的LRU算法來進行頁面淘汰,以提升緩存命中率。當Buffer Pool中再也不有空閒的緩存頁時,就須要淘汰掉部分最近不多使用的緩存頁。不過,咱們怎麼知道哪些緩存頁最近頻繁使用,哪些最近不多使用呢?呵呵,神奇的鏈表再一次派上了用場,咱們能夠再建立一個鏈表,因爲這個鏈表是爲了按照最近最少使用的原則去淘汰緩存頁的,因此這個鏈表能夠被稱爲LRU鏈表(Least Recently Used)。當咱們須要訪問某個頁時,能夠這樣處理LRU鏈表:
若是該頁不在Buffer Pool中,在把該頁從磁盤加載到Buffer Pool中的緩存頁時,就把該緩存頁包裝成節點塞到鏈表的頭部。
若是該頁在Buffer Pool中,則直接把該頁對應的LRU鏈表節點移動到鏈表的頭部。
可是這樣作會有一些性能上的問題,好比你的一次全表掃描或一次邏輯備份就把熱數據給衝完了,就會致使致使緩衝池污染問題!Buffer Pool中的全部數據頁都被換了一次血,其餘查詢語句在執行時又得執行一次從磁盤加載到Buffer Pool的操做,而這種全表掃描的語句執行的頻率也不高,每次執行都要把Buffer Pool中的緩存頁換一次血,這嚴重的影響到其餘查詢對 Buffer Pool 的使用,嚴重的下降了緩存命中率 !
因此InnoDB存儲引擎對傳統的LRU算法作了一些優化,在InnoDB中加入了midpoint。新讀到的頁,雖然是最新訪問的頁,但並非直接插入到LRU列表的首部,而是插入LRU列表的midpoint位置。這個算法稱之爲midpoint insertion stategy。默認配置插入到列表長度的5/8處。midpoint由參數innodb_old_blocks_pct控制。
midpoint以前的列表稱之爲new列表,以後的列表稱之爲old列表。能夠簡單的將new列表中的頁理解爲最爲活躍的熱點數據。
同時InnoDB存儲引擎還引入了innodb_old_blocks_time來表示頁讀取到mid位置以後須要等待多久纔會被加入到LRU列表的熱端。能夠經過設置該參數保證熱點數據不輕易被刷出。
好了,基本拿下了LRU list後,咱們繼續。前面咱們講到頁面更新是在緩存池中先進行的,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱爲髒頁(英文名:dirty page)。因此須要考慮這些被修改的頁面何時刷新到磁盤?以什麼樣的順序刷新到磁盤?固然,最簡單的作法就是每發生一次修改就當即同步到磁盤上對應的頁上,可是頻繁的往磁盤中寫數據會嚴重的影響程序的性能(畢竟磁盤慢的像烏龜同樣)。因此每次修改緩存頁後,咱們並不着急當即把修改同步到磁盤上,而是在將來的某個時間點進行同步,由後臺刷新線程依次刷新到磁盤,實現修改落地到磁盤。
可是若是不當即同步到磁盤的話,那以後再同步的時候咱們怎麼知道Buffer Pool中哪些頁是髒頁,哪些頁歷來沒被修改過呢?總不能把全部的緩存頁都同步到磁盤上吧,假如Buffer Pool被設置的很大,比方說300G,那一次性同步這麼多數據豈不是要慢死!因此,咱們不得再也不建立一個存儲髒頁的鏈表,凡是在LRU鏈表中被修改過的頁都須要加入這個鏈表中,由於這個鏈表中的頁都是須要被刷新到磁盤上的,因此也叫FLUSH鏈表,有時候也會被簡寫爲FLU鏈表。鏈表的構造和Free鏈表差很少,這就不贅述了。這裏的髒頁修改指的此頁被加載進Buffer Pool後第一次被修改,只有第一次被修改時才須要加入FLUSH鏈表(代碼中是根據Page頭部的oldest_modification == 0來判斷是不是第一次修改),若是這個頁被再次修改就不會再放到FLUSH鏈表了,由於已經存在。須要注意的是,髒頁數據實際還在LRU鏈表中,而FLUSH鏈表中的髒頁記錄只是經過指針指向LRU鏈表中的髒頁。而且在FLUSH鏈表中的髒頁是根據oldest_lsn(這個值表示這個頁第一次被更改時的lsn號,對應值oldest_modification,每一個頁頭部記錄)進行排序刷新到磁盤的,值越小表示要最早被刷新,避免數據不一致。
這三個重要列表(LRU list, free list,flush list)的關係能夠用下圖表示:
Free鏈表跟LRU鏈表的關係是相互流通的,頁在這兩個鏈表間來回置換。而FLUSH鏈表記錄了髒頁數據,也是經過指針指向了LRU鏈表,因此圖中FLUSH鏈表被LRU鏈表包裹。
說完緩衝池,下面說CheckPoint技術。
CheckPoint技術是用來解決以下幾個問題:
縮短數據庫恢復時間,重作日誌中記錄了的checkpoint的位置,這個點以前的頁已經刷新回磁盤,只須要對checkpoint以後的重作日誌進行恢復。這樣就大大縮短了恢復時間。
緩衝池不夠用時,根據LRU算法,溢出最近最少使用的頁,若是頁爲髒頁,強制執行checkpoint,將髒頁刷新回磁盤。
重作日誌不可用,是指重作日誌的這部分不能夠被覆蓋,爲何?由於:因爲重作日誌的設計是循環使用的。這部分對應的數據還未刷新到磁盤上。數據庫恢復時,若是不須要這部分日誌,便可被覆蓋;若是須要,必須強制執行checkpoint,將緩衝池中的頁至少刷新到當前重作日誌的位置。
checkpoint每次刷新多少頁到磁盤?每次從哪裏取髒頁?什麼時間觸發checkpoint?
InnoDB存儲引擎內部,兩種checkpoint,分別爲:
Sharp Checkpoint發生在數據庫關閉時,將全部的髒頁都刷新回磁盤,這是默認的工做方式,即參數:innodb_fast_shutdown=1。
不適用於數據庫運行時的刷新。
在數據庫運行時,InnoDB存儲引擎內部採用Fuzzy Checkpoint,只刷新一部分髒頁。
幾種發生Fuzzy Checkpoint的狀況:
①MasterThread Checkpoint
異步刷新,每秒或每10秒從緩衝池髒頁列表刷新必定比例的頁回磁盤。異步刷新,即此時InnoDB存儲引擎能夠進行其餘操做,用戶查詢線程不會受阻。②FLUSH_LRU_LIST Checkpoint
InnoDB存儲引擎須要保證LRU列表中差很少有100個空閒頁可供使用。在InnoDB 1.1.x版本以前,用戶查詢線程會檢查LRU列表是否有足夠的空間操做。若是沒有,根據LRU算法,溢出LRU列表尾端的頁,若是這些頁有髒頁,須要進行checkpoint。所以叫:flush_lru_list checkpoint。InnoDB 1.2.x開始,這個檢查放在了單獨的進程(Page Cleaner)中進行。好處:1.減小master Thread的壓力 2.減輕用戶線程阻塞。
設置參數:innodb_lru_scan_dept:控制LRU列表中可用頁的數量,該值默認1024
③Async/Sync Flush Checkpoint
指重作日誌不可用的狀況,須要強制刷新頁回磁盤,此時的頁時髒頁列表選取的。
這種狀況是保證重作日誌的可用性,說白了就是,重作日誌中能夠循環覆蓋的部分空間太少了,換種說法,就是極短期內產生了大量的redo log。
接下來會有幾個變量,圖解也不難,仔細看看。
InnoDB存儲引擎,經過LSN(Log Sequence Number)來標記版本,LSN是8字節的數字。每一個頁有LSN,重作日誌有LSN,checkpoint有LSN。
寫入日誌的LSN:redo_lsn
刷新回磁盤的最新頁LSN:checkpoint_lsn
有以下定義:
checkpoint_age = redo_lsn - checkpoint_lsn
async_water_mark = 75% * total_redo_file_size
sync_water_mark = 90% * total_redo_file_size
刷新過程以下圖所示:
④Dirty Page too much Checkpoint
即髒頁太多,強制checkpoint.保證緩衝池有足夠可用的頁。
參數設置:innodb_max_dirty_pages_pct = 75 表示:當緩衝池中髒頁的數量佔75%時,強制checkpoint。1.0.x以後默認75
Insert Buffer是InnoDB存儲引擎關鍵特性中最使人激動與興奮的一個功能。不過這個名字可能會讓人認爲插入緩衝是緩衝池中的一個組成部分。其實否則,InnoDB緩衝池中有Insert Buffer信息當然不錯,可是Insert Buffer和數據頁同樣,也是物理頁的一個組成部分。
通常狀況下,主鍵是行惟一的標識符。一般應用程序中行記錄的插入順序是按照主鍵遞增的順序進行插入的。所以,插入彙集索引通常是順序的,不須要磁盤的隨機讀取。由於,對於此類狀況下的插入,速度仍是很是快的。(若是主鍵類是UUID這樣的類,那麼插入和輔助索引同樣,也是隨機的。)
若是索引是非彙集的且不惟一。在進行插入操做時,數據的存放對於非彙集索引葉子節點的插入不是順序的,這時須要離散地訪問非彙集索引頁,因爲隨機讀取的存在而致使了插入操做性能降低。這是由於B+樹的特性決定了非彙集索引插入的離散性。
Insert Buffer的設計,對於非彙集索引的插入和更新操做,不是每一次直接插入到索引頁中,而是先判斷插入非彙集索引頁是否在緩衝池中,若存在,則直接插入,不存在,則先放入一個Insert Buffer對象中。數據庫這個非彙集的索引已經插到葉子節點,而實際並無,只是存放在另外一個位置。而後再以必定的頻率和狀況進行Insert Buffer和輔助索引頁子節點的merge(合併)操做,這時一般能將多個插入合併到一個操做中(由於在一個索引頁中),這就大大提升了對於非彙集索引插入的性能。
須要知足的兩個條件:
輔助索引不能是惟一的,由於在插入緩衝時,數據庫並不去查找索引頁來判斷插入的記錄的惟一性。若是去查找確定又會有離散讀取的狀況發生,從而致使Insert Buffer失去了意義。
若是說插入緩衝是爲了提升寫性能的話,那麼兩次寫是爲了提升可靠性。
介紹兩次寫以前,說一下部分寫失效:
想象這麼一個場景,當數據庫正在從內存向磁盤寫一個數據頁時,數據庫宕機,從而致使這個頁只寫了部分數據,這就是部分寫失效,它會致使數據丟失。這時是沒法經過重作日誌恢復的,由於重作日誌記錄的是對頁的物理修改,若是頁自己已經損壞,重作日誌也無能爲力。
從上面分析咱們知道,在部分寫失效的狀況下,咱們在應用重作日誌以前,須要原始頁的一個副本,兩次寫就是爲了解決這個問題,下面是它的原理圖:
兩次寫須要額外添加兩個部分:
1)內存中的兩次寫緩衝(doublewrite buffer),大小爲2MB
2)磁盤上共享表空間中連續的128頁,大小也爲2MB
其原理是這樣的:
1)當刷新緩衝池髒頁時,並不直接寫到數據文件中,而是先拷貝至內存中的兩次寫緩衝區。
2)接着從兩次寫緩衝區分兩次寫入磁盤共享表空間中,每次寫入1MB
3)待第2步完成後,再將兩次寫緩衝區寫入數據文件
這樣就能夠解決上文提到的部分寫失效的問題,由於在磁盤共享表空間中已有數據頁副本拷貝,若是數據庫在頁寫入數據文件的過程當中宕機,在實例恢復時,能夠從共享表空間中找到該頁副本,將其拷貝覆蓋原有的數據頁,再應用重作日誌便可。
其中第2步是額外的性能開銷,但因爲磁盤共享表空間是連續的,所以開銷不是很大。能夠經過參數skip_innodb_doublewrite禁用兩次寫功能,默認是開啓的,強烈建議開啓該功能。
MySQL InnoDB特性:兩次寫(DoubleWrite)
InnoDB特性之-兩次寫
哈希是一種很是快的查找方法,在通常狀況時間複雜度爲O(1)。而B+樹的查找次數,取決於B+樹的高度,在生成環境中,B+樹的高度通常爲3-4層,不須要查詢3-4次。
InnoDB存儲引擎會監控對錶上各索引頁的查詢。若是觀察到創建哈希索引能夠提高速度,這簡歷哈希索引,稱之爲自適應哈希索引(Adaptive Hash Index, AHI)。AHI是經過緩衝池的B+樹頁構造而來的。所以創建的速度很是快,且不要對整張表構建哈希索引。InnoDB存儲引擎會自動根據訪問的頻率和模式來自動的爲某些熱點頁創建哈希索引。
AHI有一個要求,對這個頁的連續訪問模式(查詢條件)必須同樣的。例如聯合索引(a,b)其訪問模式能夠有如下狀況:
WHERE a=XXX;
WHERE a=xxx AND b=xxx。
若交替進行上述兩張查詢,InnoDB存儲引擎不會對該頁構造AHI。此外AHI還有以下要求:
以該模式訪問了100次;
頁經過該模式訪問了N次,其中N=頁中記錄/16。
根據官方文檔顯示,啓用AHI後,讀取和寫入的速度能夠提升2倍,負責索引的連接操做性能能夠提升5倍。其設計思想是數據庫自由化的,無需DBA對數據庫進行人爲調整。
爲了提升磁盤操做性能,當前的數據庫系統都採用異步IO的方式來處理磁盤操做。InnoDB也是如此。
與AIO對應的是Sync IO,即每進行一次IO操做,須要等待這次操做結束才能繼續接下來的操做。可是若是用戶發出的是一條索引掃描的查詢,那麼這條SQL語句可能須要掃描多個索引頁,也就是須要進行屢次IO操做。在每掃描一個頁並等待其完成再進行下一次掃描,這是沒有必要的。用戶能夠在發出一個IO請求後當即再發出另一個IO請求,當所有IO請求發送完畢後,等待全部IO操做完成,這就是AIO。
AIO的另一個優點是進行IO Merge操做,也就是將多個IO合併爲一個IO操做,這樣能夠提升IOPS的性能。
在InnoDB 1.1.x以前,AIO的實現是經過InnoDB存儲引擎中的代碼來模擬的。可是從這以後,提供了內核級別的AIO的支持,稱爲Native AIO。Native AIO須要操做系統提供支持。Windows和Linux都支持,而Mac則未提供。在選擇MySQL數據庫服務器的操做系統時,須要考慮這方面的因素。
MySQL能夠經過參數innodb_use_native_aio來決定是否啓用Native AIO。在InnoDB存儲引擎中,read ahead方式的讀取都是經過AIO完成,髒頁的刷新,也是經過AIO完成。
InnoDB存儲引擎在刷新一個髒頁時,會檢測該頁所在區(extent)的全部頁,若是是髒頁,那麼一塊兒刷新。這樣作的好處是經過AIO能夠將多個IO寫操做合併爲一個IO操做。該工做機制在傳統機械磁盤下有顯著優點。可是須要考慮下吧兩個問題:
是否是將不怎麼髒的頁進行寫入,而該頁以後又會很快變成髒頁? 固態硬盤有很高IOPS,是否還須要這個特性? 爲此InnoDB存儲引擎1.2.x版本開始提供參數innodb_flush_neighbors來決定是否啓用。對於傳統機械硬盤建議使用,而對於固態硬盤能夠關閉。