InnoDB體系架構
上圖簡單顯示了InnoDB存儲引擎的體系架構圖中可見,InnoDB存儲引擎有多個內存塊,可以認為這些內存塊組成了一個大的內存池,負責如下工作:
維護所有進程/線程需要訪問的多個內部數據結構。
緩存磁盤上的數據,方便快速地讀取,同時在對磁盤文件的數據修改之前在這里緩存。
重做日志(redo log)緩沖
......
后臺線程的主要作用是負責刷新內存池中的數據,保證緩沖池中的內存緩存是最近的數據。此外將已修改的數據文件刷新到磁盤文件,同時保證在數據庫發生異常的情況下InnoDB能恢復到正常運行狀態。
后臺線程
InnoDB存儲引擎是多線程的模型,因此其后臺有多個不同的后臺線程,負責處理不同的任務。
1.Master Thread
Master Thread是一個非常核心的后臺線程,主要負責將緩沖池中的數據異步刷新到磁盤,保證數據的一致性,包括臟頁的刷新,合并插入緩沖、UNDO頁的回收等。
2.IO Thread
在InnoDB中大量使用了AIO來處理寫IO請求,這樣可以極大提高數據庫的性能。而IO Thread的工作主要負責這些IO請求的回調處理。
可通過命令SHOW ENGINE INNODB STATUS來觀察InnoDB中的IO Thread。
3.Purge Thread
事務被提交后,其所使用的undo log可能不再需要,因此需要PurgeThread來回收已經使用并分配的undo頁。用戶可以在MySQL數據庫的配置文件中添加如下命令來啟動獨立的Purge Thread:
innodb_purge_threads=1
4.Page Cleaner Thread
InnoDB 1.2版本中引入。其作用是將之前版本中臟頁的刷新操作都放到單獨的線程中來完成,目的是減輕原Master Thread的工作及對于用戶查詢線程的阻塞,進一步提高InnoDB存儲引擎的性能。
內存
1.緩沖池(Database Buffer Pool)
InnoDB存儲引擎是基于磁盤存儲的,并將其中的記錄按照頁的方式進行管理。在數據庫管理系統中,由于CPU速度與磁盤速度之間的鴻溝,基于磁盤的數據庫系統通常使用緩沖池技術來提高數據庫的整體性能。
緩存池簡單來說就是一塊內存區域,通過內存的速度來彌補磁盤速度較慢對數據庫性能的影響。在數據庫中進行讀取頁的操作,首先將從磁盤讀到的頁存放在緩沖池中,這個過程稱為“FIX”在緩沖池中。下一次再讀相同的頁時,首先判斷該頁是否在緩沖池中。若在緩沖池中,稱為頁在緩沖池中被命中,直接讀取該頁。否則,讀取磁盤上的頁。
對于數據庫中頁的修改操作,則首先修改在緩沖池中的頁,然后再以一定的頻率刷新到磁盤上(并不是每次頁發生更新時刷新,而是通過一種稱為CheckPoint的機制刷新回磁盤)。
綜上所述,緩沖池的大小直接影響數據庫的整體性能。對于InnoDB而言,通過參數innodb_buffer_pool_size來設置。
緩沖池中緩存的數據頁類型有:索引頁、數據頁、undo頁、插入緩沖、自適應哈希索引、InnoDB存儲的鎖信息、數據字典信息等。
InnoDB允許有多個緩沖池實例。每個頁根據哈希值平均分配到不同的緩沖池實例中,好處是減少數據庫內部的資源競爭,增加數據庫的并發能力。
2.LRU List、Free List和Flush List
通常來說,數據庫中的緩沖池是通過LRU(Latest Recent Used)算法來進行管理的。即最頻繁使用的頁在LRU列表的前端,而最少使用的頁在LRU列表的尾端。當緩沖池不能存放新讀取到的頁時,將首先釋放LRU列表中尾端的頁。
在InnoDB中,對傳統的LRU算法做了一些優化,添加了midpoint位置。新讀取到的頁不是直接放到LRU列表的首部,而是放到midpoint位置。在默認配置下,該位置在LRU列表長度的5/8處。通過參數innodb_old_blocks_pct控制。
在InnoDB中,把midpoint之后的列表稱為old列表,之前的列表稱為new列表。InnoDB引入另一個參數innodb_old_blocks_time來管理列表,用于表示頁讀取到mid位置后需要等待多久才會被加入到LRU列表的熱端。
LRU列表用來管理已經讀取的頁,但當數據庫剛啟動時,LRU列表是空的,即沒有任何的頁。這時頁先存放在Free列表中。當需要從緩沖池中分頁時,首先從Free列表中查找是否有可用的空閑頁若有則將該頁從Free列表中刪除,放入到LRU列表中。否則,根據LRU算法,淘汰LRU列表末尾的頁,將該內存空間分配給新的頁。當頁從LRU列表的old部分加入到new部分時,稱此時的操作為page made young,而因為innodb_old_blocks_time的設置而導致頁沒有從old部分移動到new部分的操作稱為page not made young。
可以通過命令SHOW ENGINE INNODB STATUS來查看LRU列表及FREE列表的使用情況和運行狀態。
其中Buffer pool size表示緩沖池中頁的數量。
Free Buffers表示當前Free列表中頁的數量。
Database pages表示LRU列表中頁的數量。
pages made young顯示了LRU列表中頁移動到前端的次數。
Buffer pool hit rate表示緩沖池命中率,通常該值不應該小于95%,否則需要觀察是否由于全表掃描引起的LRU列表被污染。
Modified db pages 表示臟頁的數量。
在LRU列表中的頁被修改后,稱該頁為臟頁,即緩沖池中的頁和磁盤上的頁的數據產生了不一致。這時數據庫會通過CHECKPOINT機制將臟頁刷新回磁盤,而Flush列表中的頁即為臟頁列表。需要注意的是,臟頁既存在于LRU列表中,也存在于Flush列表中,二者互不影響。
3.重做日志緩沖
InnoDB存儲引擎首先將重做日志信息先放入到這個緩沖區,然后按一定的頻率將其刷新到重做日志文件。重做日志緩沖一般不需要設置得很大,因為一般每秒就會刷新一次。
重做日志在下列三種情況下會將緩沖內容刷新到外部磁盤的重做日志文件中:
Master Thread每一秒將重做日志緩沖刷新到重做日志文件。
每個事務提交時會將重做日志緩沖刷新到重做日志文件。
當重做日志緩沖池剩余空間小于1/2時,重做日志緩沖刷新到重做日志文件。
4.額外的內存池
在InnoDB存儲引擎中,對內存的管理是通過一種稱為內存堆的方式進行的。在對一些數據結構本身的內存進行分配時,需要從額外的內存池中進行申請,當該區域的內存不夠時會從緩沖池中進行申請。例如,分配了緩沖池,但是每個緩沖池中的幀緩沖還有對應的緩沖控制對象,這些對象記錄了一些諸如LRU、鎖、等待等信息,而這個對象的內存需要從額外內存池中申請。因此申請了很大的InnoDB緩沖池時,也需要考慮相應增加這個值。
Checkpoint技術
為了避免每次頁發送變化就將其刷新至磁盤,采用了Checkpoint技術統一對刷新進行管理。但是為了防止刷新時發生宕機引起數據丟失的情況,數據庫系統普遍采用了Write Ahead Log策略,即當前事務提交時,先寫重做日志,再修改頁。這樣宕機導致數據丟失時,可以通過重做日志來完成數據的恢復。
Checkpoint技術是為了解決以下幾個問題:
縮短數據庫的恢復時間
緩沖池不夠用時,將臟頁刷新到磁盤
重做日志不可用時,刷新臟頁
當數據庫發生宕機時,數據庫不需要重做所有的日志,因為Checkpoint之前的頁都已經刷新回磁盤。只需要對Checkpoint后的重做日志進行恢復。這樣就大大縮短了恢復時間。
當緩沖池不夠用時,根據LRU算法會溢出最近最少使用的頁,若此頁為臟頁,那么需要強制執行Checkpoint,將臟頁刷回磁盤。
重做日志出現不可用的情況是因為當前事務數據庫系統對重做日志的設計都是循環使用的,并不是無限增大的。因此為了保證重做日志正常的循環使用,需要將臟頁及時刷新到磁盤。
InnoDB存儲引擎內部,有兩種CheckPoint,分別為:Sharp Checkpoint、Fuzzy Checkpoint。
Sharp Checkpoint發生在數據庫關閉時將所有的臟頁都刷新回磁盤,這時默認的工作方式。
但是數據庫運行并不會將所有的臟頁同時刷新回磁盤,在InnoDB內部使用Fuzzy Checkpoint進行頁的刷新,即只刷新一部分臟頁,而不是刷新所有的臟頁回磁盤。
以下幾種情況會發生Fuzzy Checkpoint:
Master Thread Checkpoint
每秒或者每十秒從緩沖池的臟頁列表中刷新一定比例的頁回磁盤。這個過程是異步的,即此時InnoDB存儲引擎可以進行其他的操作,用戶查詢線程不會阻塞。
FLUSH_LRU_LIST Checkpoint
倘若LRU列表中沒有100個空閑頁可用,引擎會將LRU列表尾端的頁移除。如果這些頁中有臟頁,那么需要進行Checkpoint。MySQL5.6版本后,這個檢查放在了一個單獨的Page Cleaner線程中,并且用戶可以通過參數innodb_lru_scan_depth控制列表中可用頁的數量。
Async/Synv Flush Checkpoing
重做日志文件不可用的情況下,這時需要強制將一些頁刷新回磁盤。MySQL5.6版本后,這部分刷新操作同樣放入到單獨的Page Cleaner Thread,故不會阻塞用戶查詢進程。
Dirty Page too much Checkpoint
臟頁的數量太多,導致InnoDB存儲引擎強制進行Checkpoint,可以通過參數innodb_max_dirty_pages_pct控制。
Master Thread工作方式
InnoDB 1.0版本之前,Master Thread具有最高的線程優先級別。其內部由多個循環組成:主循環、后臺循環、刷新循環、暫停循環。Master Thread會根據數據庫運行的狀態在其中進行切換。
主循環大概每秒一次或每10秒一次,每次的操作包括:
日志緩沖刷新到磁盤,即使這個事務還沒有提交(總是);
合并插入緩沖(可能);
至多刷新100個InnoDB的緩沖池中的臟頁到磁盤(可能);
如果當前沒有用戶活動,切換到backgroud loop(可能)。
當前沒有用戶活動或者數據庫關閉時,會切換到后臺循環。后臺循環會執行以下操作:
刪除無用的Undo頁(總是);
合并20個插入緩沖(總是);
跳回到主循環(總是);
不斷刷新100個頁直到符合條件(可能)。
若刷新循環中也沒什么事情可以做了,InnoDB存儲引擎會切換到暫停循環,將Master Thread掛起。
InnoDB 1.2.x之前版本的Master Thread
在了解了1.0.x版本之前的Master Thread的具體實現過程后,細心的讀者會發現InnoDB存儲引擎對于IO其實是有限制的,在緩沖池向磁盤刷新時其實都做了一定的硬編碼(hard coding)。在磁盤技術飛速發展的今天,當固態磁盤(SSD)出現時,這種規定在很大程度上限制了InnoDB存儲引擎對磁盤IO的性能,尤其是寫入性能。
從前面的偽代碼來看,無論何時,InnoDB存儲引擎最大只會刷新100個臟頁到磁盤,合并20個插入緩沖。如果是在寫入密集的應用程序中,每秒可能會產生大于100個的臟頁,如果是產生大于20個插入緩沖的情況,Master Thread似乎會“忙不過來”,或者說它總是做得很慢。即使磁盤能在1秒內處理多于100個頁的寫入和20個插入緩沖的合并,但是由于hard coding,Master Thread也只會選擇刷新100個臟頁和合并20個插入緩沖。同時,當發生宕機需要恢復時,由于很多數據還沒有刷新回磁盤,會導致恢復的時間可能需要很久,尤其是對于insert buffer來說。
InnoDB Plugin(從InnoDB1.0.x版本開始)提供了參數innodb_io_capacity,用來表示磁盤IO的吞吐量,默認值為200。對于刷新到磁盤頁的數量,會按照innodb_io_capacity的百分比來進行控制。規則如下:
在合并插入緩沖時,合并插入緩沖的數量為innodb_io_capacity值的5%;
在從緩沖區刷新臟頁時,刷新臟頁的數量為innodb_io_capacity。
若用戶使用了SSD類的磁盤,或者將幾塊磁盤做了RAID,當存儲設備擁有更高的IO速度時,完全可以將innodb_io_capacity的值調得再高點,直到符合磁盤IO的吞吐量為止。
另一個問題是,參數innodb_max_dirty_pages_pct默認值的問題,在InnoDB 1.0.x版本之前,該值的默認為90,意味著臟頁占緩沖池的90%。但是該值“太大”了,因為InnoDB存儲引擎在每秒刷新緩沖池和flush loop時會判斷這個值,如果該值大于innodb_max_dirty_pages_pct,才刷新100個臟頁,如果有很大的內存,或者服務器的壓力很大,這時刷新臟頁的速度反而會降低。同樣,在恢復階段可能需要更多的時間。
InnoDB 1.0.x版本帶來的另一個參數是innodb_adaptive_flushing(自適應地刷新),該值影響每秒刷新臟頁的數量。原來的刷新規則是:臟頁在緩沖池所占的比例小于innodb_max_dirty_pages_pct時,不刷新臟頁;大于innodb_max_dirty_pages_pct時,刷新100個臟頁。隨著innodb_adaptive_flushing參數的引入,InnoDB存儲引擎會通過一個名為buf_flush_get_desired_flush_rate的函數來判斷需要刷新臟頁最合適的數量。粗略地翻閱源代碼后發現buf_flush_get_desired_flush_rate通過判斷產生重做日志(redo log)的速度來決定最合適的刷新臟頁數量。因此,當臟頁的比例小于innodb_max_dirty_pages_pct時,也會刷新一定量的臟頁。
InnoDB 1.2.x版本的Master Thread
在InnoDB 1.2.x版本中再次對Master Thread進行了優化,由此也可以看出Master Thread對性能所起到的關鍵作用。在InnoDB 1.2.x版本中,Master Thread的偽代碼如下:
if InnoDB is idle
srv_master_do_idle_tasks();
else
srv_master_do_active_tasks();
其中srv_master_do_idle_tasks()就是之前版本中每10秒的操作,srv_master_do_active_tasks()處理的是之前每秒中的操作。同時對于刷新臟頁的操作,從Master Thread線程分離到一個單獨的Page Cleaner Thread,從而減輕了Master Thread的工作,同時進一步提高了系統的并發性。
InnoDB關鍵特性
InnoDB存儲引擎的關鍵特性包括:
插入緩沖(Insert Buffer)
兩次寫(Double Write)
自適應哈希索引(Adaptive Hash Index)
異步IO(Async IO)
刷新鄰接頁(Flush Neighbor Page)
上述這些特性為InnoDB存儲引擎帶來更好的性能以及更高的可靠性。
插入緩沖
1.Insert Buffer
這個名字可能會讓人認為插入緩沖是緩沖池中的一個組成部分。其實不然,InnoDB緩沖池中有Insert Buffer信息固然不錯,但是Insert Buffer和數據頁一樣,也是物理頁的一個組成部分。
在InnoDB存儲引擎中,主鍵是行唯一的標識符。通常應用程序中行記錄的插入順序是按照主鍵遞增的順序進行插入的。因此,插入聚集索引(Primary Key)一般是順序的,不需要磁盤的隨機讀取。比如按下列SQL定義表:
CREATE TABLE t (
a INT AUTO_INCREMENT,
b VARCHAR(30),
PRIMARY KEY(a));
其中a列是自增長的,若對a列插入NULL值,則由于其具有AUTO_INCREMENT屬性,其值會自動增長。同時頁中的行記錄按a的值進行順序存放。在一般情況下,不需要隨機讀取另一個頁中的記錄。因此,對于這類情況下的插入操作,速度是非常快的。
注意
并不是所有的主鍵插入都是順序的。若主鍵類是UUID這樣的類,那么插入和輔助索引一樣,同樣是隨機的。即使主鍵是自增類型,但是插入的是指定的值,而不是NULL值,那么同樣可能導致插入并非連續的情況。
但是不可能每張表上只有一個聚集索引,更多情況下,一張表上有多個非聚集的輔助索引(secondary index)。比如,用戶需要按照b這個字段進行查找,并且b這個字段不是唯一的。
在這樣的情況下產生了一個非聚集的且不是唯一的索引。在進行插入操作時,數據頁的存放還是按主鍵a進行順序存放的,但是對于非聚集索引葉子節點的插入不再是順序的了,這時就需要離散地訪問非聚集索引頁,由于隨機讀取的存在而導致了插入操作性能下降。當然這并不是這個b字段上索引的錯誤,而是因為B+樹的特性決定了非聚集索引插入的離散性。
需要注意的是,在某些情況下,輔助索引的插入依然是順序的,或者說是比較順序的,比如用戶購買表中的時間字段。在通常情況下,用戶購買時間是一個輔助索引,用來根據時間條件進行查詢。但是在插入時卻是根據時間的遞增而插入的,因此插入也是“較為”順序的。
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來查看插入緩沖的信息。
正如前面所說的,目前Insert Buffer存在一個問題是:在寫密集的情況下,插入緩沖會占用過多的緩沖池內存(innodb_buffer_pool),默認最大可以占用到1/2的緩沖池內存。
這對于其他的操作可能會帶來一定的影響,修改IBUF_POOL_SIZE_PER_MAX_SIZE就可以對插入緩沖的大小進行控制。比如將IBUF_POOL_SIZE_PER_MAX_SIZE改為3,則最大只能使用1/3的緩沖池內存。
2.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操作的第一個過程,即將記錄標記為刪除。Purge Buffer對應UPDATE操作的第二個過程,即將記錄真正的刪除。同時,InnoDB存儲引擎提供了參數innodb_change_buffering,用來開啟各種Buffer的選項。該參數可選的值為:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面討論過的三種情況。changes表示啟用inserts和deletes,all表示啟用所有,none表示都不啟用。該參數默認值為all。
從InnoDB 1.2.x版本開始,可以通過參數innodb_change_buffer_max_size來控制Change Buffer最大使用內存的數量。
兩次寫
如果說Insert Buffer帶給InnoDB存儲引擎的是性能上的提升,那么doublewrite(兩次寫)帶給InnoDB存儲引擎的是數據頁的可靠性。
當發生數據庫宕機時,可能InnoDB存儲引擎正在寫入某個頁到表中,而這個頁只寫了一部分,比如16KB的頁,只寫了前4KB,之后就發生了宕機,這種情況被稱為部分寫失效(partial page write)。在InnoDB存儲引擎未使用doublewrite技術前,曾經出現過因為部分寫失效而導致數據丟失的情況。
有經驗的DBA也許會想,如果發生寫失效,可以通過重做日志進行恢復。這是一個辦法。但是必須清楚地認識到,重做日志中記錄的是對頁的物理操作,如偏移量800,寫'aaaa'記錄。如果這個頁本身已經發生了損壞,再對其進行重做是沒有意義的。這就是說,在應用(apply)重做日志前,用戶需要一個頁的副本,當寫入失效發生時,先通過頁的副本來還原該頁,再進行重做,這就是doublewrite。在InnoDB存儲引擎中doublewrite的體系架構如下圖所示。
doublewrite由兩部分組成,一部分是內存中的doublewrite buffer,大小為2MB,另一部分是物理磁盤上共享表空間中連續的128個頁,即2個區(extent),大小同樣為2MB。在對緩沖池的臟頁進行刷新時,并不直接寫磁盤,而是會通過memcpy函數將臟頁先復制到內存中的doublewrite buffer,之后通過doublewrite buffer再分兩次,每次1MB順序地寫入共享表空間的物理磁盤上,然后馬上調用fsync函數,同步磁盤,避免緩沖寫帶來的問題。在這個過程中,因為doublewrite頁是連續的,因此這個過程是順序寫的,開銷并不是很大。在完成doublewrite頁的寫入后,再將doublewrite buffer中的頁寫入各個表空間文件中,此時的寫入則是離散的。
若查看MySQL官方手冊,會發現在命令SHOW GLOBAL STATUS中Innodb_buffer_pool_pages_flushed變量表示當前從緩沖池中刷新到磁盤頁的數量。根據之前的介紹,用戶應該了解到,在默認情況下所有頁的刷新首先都需要放入到doublewrite中,因此該變量應該和Innodb_dblwr_pages_written一致。然而在MySQL 5.5.24版本之前,Innodb_buffer_pool_pages_flushed總是為Innodb_dblwr_pages_written的2倍,而此Bug直到MySQL5.5.24才被修復。因此用戶若需要統計數據庫在生產環境中寫入的量,最安全的方法還是根據Innodb_dblwr_pages_written來進行統計,這在所有版本的MySQL數據庫中都是正確的。
參數skip_innodb_doublewrite可以禁止使用doublewrite功能,這時可能會發生前面提及的寫失效問題。不過如果用戶有多個從服務器(slave server),需要提供較快的性能(如在slaves erver上做的是RAID0),也許啟用這個參數是一個辦法。不過對于需要提供數據高可靠性的主服務器(master server),任何時候用戶都應確保開啟doublewrite功能。
自適應哈希索引
哈希(hash)是一種非常快的查找方法,在一般情況下這種查找的時間復雜度為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
訪問模式一樣指的是查詢的條件一樣,若交替進行上述兩種查詢,那么InonDB存儲引擎不會對該頁構造AHI。此外AHI還有如下的要求:
以該模式訪問了100次
頁通過該模式訪問了N次,其中N=頁中記錄*1/16
根據InnoDB存儲引擎官方的文檔顯示,啟用AHI后,讀取和寫入速度可以提高2倍,輔助索引的連接操作性能可以提高5倍。毫無疑問,AHI是非常好的優化模式,其設計思想是數據庫自優化的(self-tuning),即無需DBA對數據庫進行人為調整。
值得注意的是,哈希索引只能用來搜索等值的查詢,如SELECT*FROM table WHERE index_col='xxx'。而對于其他查找類型,如范圍查找,是不能使用哈希索引的。
由于AHI是由InnoDB存儲引擎控制的,因此這里的信息只供用戶參考。不過用戶可以通過觀察SHOW ENGINE INNODB STATUS的結果及參數innodb_adaptive_hash_index來考慮是禁用或啟動此特性,默認AHI為開啟狀態。
異步IO
為了提高磁盤操作性能,當前的數據庫系統都采用異步IO(Asynchronous IO,AIO)的方式來處理磁盤操作。InnoDB存儲引擎亦是如此。
與AIO對應的是Sync IO,即每進行一次IO操作,需要等待此次操作結束才能繼續接下來的操作。但是如果用戶發出的是一條索引掃描的查詢,那么這條SQL查詢語句可能需要掃描多個索引頁,也就是需要進行多次的IO操作。在每掃描一個頁并等待其完成后再進行下一次的掃描,這是沒有必要的。用戶可以在發出一個IO請求后立即再發出另一個IO請求,當全部IO請求發送完畢后,等待所有IO操作的完成,這就是AIO。
AIO的另一個優勢是可以進行IO Merge操作,也就是將多個IO合并為1個IO,這樣可以提高IOPS的性能。例如用戶需要訪問頁的(space,page_no)為:
(8,6)、(8,7),(8,8)
每個頁的大小為16KB,那么同步IO需要進行3次IO操作。而AIO會判斷到這三個頁是連續的,因此AIO底層會發送一個IO請求,從(8,6)開始,讀取48KB的頁。
在InnoDB1.1.x之前,AIO的實現通過InnoDB存儲引擎中的代碼來模擬實現。而從InnoDB 1.1.x開始(InnoDB Plugin不支持),提供了內核級別AIO的支持,稱為Native AIO。因此在編譯或者運行該版本MySQL時,需要libaio庫的支持。
需要注意的是,Native AIO需要操作系統提供支持。Windows系統和Linux系統都提供Native AIO支持,而Mac OSX系統則未提供。因此在這些系統下,依舊只能使用原模擬的方式。
參數innodb_use_native_aio用來控制是否啟用Native AIO,在Linux操作系統下,默認值為ON:
用戶可以通過開啟和關閉Native AIO功能來比較InnoDB性能的提升。官方的測試顯示,啟用Native AIO,恢復速度可以提高75%。
在InnoDB存儲引擎中,read ahead方式的讀取都是通過AIO完成,臟頁的刷新,即磁盤的寫入操作則全部由AIO完成。
刷新鄰接頁
InnoDB存儲引擎還提供了Flush Neighbor Page(刷新鄰接頁)的特性。其工作原理為:當刷新一個臟頁時,InnoDB存儲引擎會檢測該頁所在區(extent)的所有頁,如果是臟頁,那么一起進行刷新。這樣做的好處顯而易見,通過AIO可以將多個IO寫入操作合并為一個IO操作,故該工作機制在傳統機械磁盤下有著顯著的優勢。但是需要考慮到下面兩個問題:
是不是可能將不怎么臟的頁進行了寫入,而該頁之后又會很快變成臟頁?
固態硬盤有著較高的IOPS,是否還需要這個特性?
為此,InnoDB存儲引擎從1.2.x版本開始提供了參數innodb_flush_neighbors,用來控制是否啟用該特性。對于傳統機械硬盤建議啟用該特性,而對于固態硬盤有著超高IOPS性能的磁盤,則建議將該參數設置為0,即關閉此特性。
啟動、關閉與恢復
InnoDB是MySQL的存儲引擎之一,因此InnoDB存儲引擎的啟動和關閉,更準確的是指在MySQL實例的啟動過程中對InnoDB存儲引擎的處理過程。
在關閉時,參數innodb_fast_shutdown影響著表的存儲引擎為InnoDB的行為。該參數可取值為0、1、2,默認值為1。
0表示在MySQL數據庫關閉時,InnoDB需要完成所有的full purge和merge insert buffer,并且將所有的臟頁刷新回磁盤。這需要一些時間,有時甚至需要幾個小時來完成。如果在進行InnoDB升級時,必須將這個參數調為0,然后再關閉數據庫。
1是參數innodb_fast_shutdown的默認值,表示不需要完成上述的full purge和merge insert buffer操作,但是在緩沖池中的一些數據臟頁還是會刷新回磁盤。
2表示不完成full purge和merge insert buffer操作,也不將緩沖池中的數據臟頁寫回磁盤,而是將日志都寫入日志文件。這樣不會有任何事務的丟失,但是下次MySQL數據庫啟動時,會進行恢復操作(recovery)。
當正常關閉MySQL數據庫時,下次的啟動應該會非常“正常”。但是如果沒有正常地關閉數據庫,如用kill命令關閉數據庫,在MySQL數據庫運行中重啟了服務器,或者在關閉數據庫時,將參數innodb_fast_shutdown設為了2時,下次MySQL數據庫啟動時都會對InnoDB存儲引擎的表進行恢復操作。
參數innodb_force_recovery影響了整個InnoDB存儲引擎恢復的狀況。該參數值默認為0,代表當發生需要恢復時,進行所有的恢復操作,當不能進行有效恢復時,如數據頁發生了corruption,MySQL數據庫可能發生宕機(crash),并把錯誤寫入錯誤日志中去。
但是,在某些情況下,可能并不需要進行完整的恢復操作,因為用戶自己知道怎么進行恢復。比如在對一個表進行alter table操作時發生意外了,數據庫重啟時會對InnoDB表進行回滾操作,對于一個大表來說這需要很長時間,可能是幾個小時。這時用戶可以自行進行恢復,如可以把表刪除,從備份中重新導入數據到表,可能這些操作的速度要遠遠快于回滾操作。
參數innodb_force_recovery還可以設置為6個非零值:1~6。大的數字表示包含了前面所有小數字表示的影響。具體情況如下:
1(SRV_FORCE_IGNORE_CORRUPT):忽略檢查到的corrupt頁。
2(SRV_FORCE_NO_BACKGROUND):阻止Master Thread線程的運行,如Master Thread線程需要進行full purge操作,而這會導致crash。
3(SRV_FORCE_NO_TRX_UNDO):不進行事務的回滾操作。
4(SRV_FORCE_NO_IBUF_MERGE):不進行插入緩沖的合并操作。
5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤銷日志(Undo Log),InnoDB存儲引擎會將未提交的事務視為已提交。
6(SRV_FORCE_NO_LOG_REDO):不進行前滾的操作。
需要注意的是,在設置了參數innodb_force_recovery大于0后,用戶可以對表進行select、create和drop操作,但insert、update和delete這類DML操作是不允許的。
InnoDB表
索引組織表
在InnoDB存儲引擎中,表都是根據主鍵順序組織存放的,這種存儲方式的表稱為索引組織表。每個表都有主鍵,如果在創建表時沒有顯示定義主鍵,則會按照如下方式選擇或者創建主鍵:
a.判定是否有非空的唯一索引,如果有則該列即為主鍵。若果有多個,則選擇建表是第一個定義的非空位于索引為主鍵。注意:主鍵的選擇根據的是定義索引的順序,而不是建表時的列的順序。
?b.如果不存在唯一索引,InnoDB存儲引擎字段創建一個6字節大小的指針。
InnoDB邏輯存儲結構
從InnoDB存儲引擎的邏輯存儲結構看,所有數據都被邏輯地存放在一個空間中,稱為表空間。表空間又由段(segment)、區(extent)、頁(page)組成。頁在一些文檔中有時也稱為塊(block),InnoDB存儲引擎的邏輯存儲結構大致如圖:
表空間
表空間可以看做時InnoDB存儲引擎邏輯結構的最高層,所有的數據都存放在表空間中。默認情況下InnoDB只有一個共享表空間ibdata1,即所有的數據都存放在這個表空間中。如果用戶啟用了innodb_file_per_table,則每張表內的數據可以單獨放到一個表空間內。
需要注意的是,啟用了innodb_file_per_table參數,每張表的表空間內存放的只是數據、索引和插入緩沖Bitmap頁,其他類的數據,如回滾信息,插入緩沖索引頁、系統事務信息,二次寫緩沖等還是存放在原來的共享表空間內。
段
表空間是由各個段組成的,常見的段有數據段、索引段、回滾段等。因為InnoDB引擎表是索引組織的,因此數據即索引,索引即數據。那么數據段即為B+樹的葉子結點,索引段即為B+樹的非葉子結點。回滾段較為特殊,后面進行介紹。
區
區是由連續頁組成的空間,在任何情況下每個區的大小都為1MB。為了保證區中頁的連續性,InnoDB一次從磁盤申請4-5個區。在默認情況下,InnoDB存儲引擎頁的大小為16KB,即一個區中一共有64個連續的頁。
InnoDB 1.0版本開始引入壓縮頁,每個頁的大小可以設置為2K、4K、8K。
InnoDB 1.2版本新增參數innodb_page_size,可將默認頁的大小設置為4K、8K。
頁
頁是InnoDB磁盤管理的最小單位,在InnoDB存儲引擎中,默認每個頁的大小為16KB。
在InnoDB存儲引擎中,常見的頁類型有:
數據頁
undo頁
系統頁
事務數據頁
插入緩沖位圖頁
插入緩沖空閑列表頁
未壓縮的二進制大對象頁
壓縮的二進制大對象頁
行
InnoDB數據是按照行進行存放的。每個頁存放的行記錄也是有硬性定義的,最多允許存放16KB/ 2 - 200行的記錄,即7992行記錄。
InnoDB行記錄格式
InnoDB存儲引擎記錄是以行的形式存儲的。這意味著頁中保存著表中一行行的數據。
Compact行記錄格式
compact行記錄是由MySQL5.0引入的,其設計目標是高效地存儲數據。簡單來說一個頁中存放的行數據越多,其性能就越高。
compact行記錄格式的首部是一個非Null變長字段長度列表,并且其是按照列的順序逆序放置的,其長度為:
若列的長度小于255,用一字節表示;
若大于255字節,用2字節表示。
所以VARCHAR類型的最大長度限制為65535。
之后的第二個部分是NULL標志位,該位指示了該行數據中是否有NULL值,有則用1表示,占用1字節。
接下來的部分是記錄頭信息,固定5字節,具體含義見圖:
最后的部分就是實際存儲每個列的數據。需要注意的是,NULL不占該部分任何空間,即NULL除了占有NULL標志位,實際存儲不占有任何空間。另外有一點需要注意的是,每行數據除了用戶定義的列外,還有兩個隱藏列,事務ID列和回滾指針列,分別為6字節和7字節的大小。若InnoDB表沒有定義主鍵,每行還會增加一個6字節的rowid列。
行溢出數據
InnoDB存儲引擎可以將一條記錄中的某些數據存儲在真正的數據頁面之外。一般默認BLOB、LOB這類的大對象列類型就會存放在數據頁面之外。但他們也可以不將數據放在溢出頁面,即便是VARCHAR列數據類型,依然有可能被存放在行溢出數據。
在一般情況下,InnoDB存儲引擎的數據都是存放在頁類型為B-tree node中。但是當發生行溢出時,數據存放在頁類型為Uncompress BLOB頁中。
InnoDB表是索引組織的,即B+Tree的結構,這樣每個頁中至少有兩條行記錄。因此,如果頁中只能存放下一條記錄,那么InnoDB存儲引擎會自動將行數據存放到溢出頁中。
InnoDB數據頁結構
InnoDB數據頁由以下七個部分組成,如圖所示:
File Header(文件頭)
Page Header(頁頭)
Infimun + Supremum Records
User Records(用戶記錄)
Free Space(空閑空間)
Page Directory(頁目錄)
File Trailer(文件結尾信息)
File Header、Page Header、File Trailer的大小是固定的,用來標示該頁的一些信息。其余部分為實際的行存儲空間,因此大小是動態的。
Infimun + Supremum Records
Infimun Supremum Records用來限定記錄的邊界,Infimun記錄是比該頁中任何主鍵值都小的值,Supremum記錄是比該頁中任何主鍵值都大的值。
File Trailer
為了保證頁能夠完整地寫入磁盤(如寫入過程中遇到宕機、磁盤損壞等原因),InnoDB存儲引擎的頁中設置了File Trailer部分。
約束
數據完整性
關系型數據庫和文件系統的一個不同點是,關系數據庫本身能保證存儲數據的完整性,不需要應用程序的控制,而文件系統一般需要在程序端進行控制。幾乎所有的關系型數據庫都提供約束機制,約束提供了一條強大而簡易的途徑來保證數據庫中的數據完整性,數據完整性有三種形式:
實體完整性:保證表中有一個主鍵。在InnoDB中,我們可以通過定義Primary Key或者Unique Key約束來保證實體的完整性。
域完整性:保證數據的值滿足特定的條件。在InnoDB引擎中,域完整性通過以下幾種途徑來保證:選擇合適的數據類型可以確保一個數據值滿足特定條件,外鍵約束,編寫觸發器,還可以考慮DEFAULT約束作為強制域完整性的一個方面。
參照完整性:保證兩張表之間的關系。InnoDB引擎支持外鍵允許用戶定義外鍵以強制參照完整性。
對于InnoDB存儲引擎,提供了以下幾種約束:
Primary Key
Unique Key
Foreign Key
Default
NOT NULL
約束可以在表建立時就進行定義,也可以在之后使用ALTER TABLE命令來進行創建。
約束和索引的概念有所不同,約束更是一個邏輯的概念,用來保證數據的完整性,而索引是一個數據結構,有邏輯上的概念,在數據庫中更是一個物理存儲的方式。
默認情況下,MySQL數據庫允許非法或者不正確的數據插入或更新,或者內部將其轉化為一個合法的值,如NOT NULL字段插入一個NULL值,會將其更改為0再進行插入,因此本身沒有對數據的正確性進行約束。
MySQL不支持傳統的CHECK約束,但是通過ENUM和SET類型可以解決部分這樣的約束需求
觸發器與約束
前面小結介紹了,完整性約束通常也可以使用觸發器來實現。
觸發器的作用是在INSERT、DELETE、和UPDATE命令之前或之后自動調用SQL命令或者存儲過程。
創建觸發器的命令是CREATE TRIGGER,只有具備Super權限的MySQL用戶才可以執行這條命令。
最多可以為一個表建立6個觸發器,即分別為INSERT、UPDATE、DELETE的BEFORE和AFTER各定義一個。
外鍵
外鍵用來保證參照完整性,MySQL的MyISAM引擎本身不支持外鍵,對于外鍵的定義只是起到一個注釋的作用。InnoDB引擎則支持外鍵約束。
我們可以在建表時就添加外鍵,也可以在之后通過ALTER TABLE命令添加。
視圖
視圖是一個命名的虛表,它由一個查詢來定義,可以當做表使用。與持久表不同的是,視圖中的數據沒有物理表現的形式。
視圖的主要用途之一是被用做一個抽象裝置,特別是對于一些應用程序,程序本身不需要關系基表的結構,只需要按照視圖定義來獲取數據或者更新數據。
雖然視圖是基于基表的一個虛擬表,但是我們可以對某些視圖進行更新操作,其實就是通過視圖的定義來更新基本表。
分區表
分區功能并不是在存儲引擎層完成的,因此不只有InnoDB存儲引擎支持分區,常見的存儲引擎MyISAM、NDB等都支持。但也并不是所有的存儲引擎都支持。
MySQL在5.1版本時添加了對于分區的支持這個過程是將一個表或者索引物分解為多個更小、更可管理的部分。就訪問數據庫的應用而言,從邏輯上講,只有一個表或者一個索引,但是在物理上這個表或者索引可能由數十個物理分區組成。每個分區都是獨立的對象,可以獨自處理,也可以作為一個更大的對象的一部分進行處理。
MySQL數據庫支持的分區類型為水平,并不支持垂直分區。此外,MySQL數據庫的分區是局部分區索引,一個分區中既存放了數據有存放了索引。
分區對于某些SQL語句性能可能會帶來提高,但是分區主要用于高可用性,利于數據庫的管理。
當前MySQL數據庫支持以下幾種類型的分區:
RANGE分區:行數據基于屬于一個給定連續區間的列值放入分區。
LIST分區:和RANGE分區類似,只是LIST分區面向的是離散的值。
HASH分區:根據用戶自定義的表達式的返回值來進行分區,返回值不能為負數。
KEY分區:根據MySQL數據庫提供的哈希函數來進行分區。
不論創建何種類型的分區,如果表中存在主鍵或者是唯一索引時,分區列必須是唯一索引的一個組成部分。
Columns分區
前面介紹的幾種分區中,分區的條件必須是整形。MySQL 5.5版本開始支持Columns分區,可以視為RANGE和LIST分區的一種進化。
Columns分區支持以下的數據類型:
所有整形類型
日期類型,如DATE和DATETIME
字符串類型,如CHAR、VARCHAR、BINARY和VARBINARY。BLOB和TEXT類型不予支持。
子分區
子分區是在分區的基礎上再進行分區,有時也稱這種分區為符合分區。MySQL允許RANGE和LIST的分區上再進行HASH或者是KEY的子分區。
分區性能
數據庫的應用分為兩類:一類是OLTP(在線事務處理),如博客、電子商務、網絡游戲等;另一類是OLAP(在線分析處理),如數據倉庫、數據集市。
對于OLAP的應用,分區的確可以很好地提高查詢的性能,因為OLAP應用的大多數查詢需要頻繁地掃描一張很大的表。假設有一張1億行的表,其中有一個時間戳屬性列。你的查詢需要從這張表中獲取一年的數據。如果按時間戳進行分區,則只需要掃描相應的分區即可。
對于OLTP的應用,分區應該非常小心。在這種應用下,不可能會獲取一張大表中10%的數據,大部分都是通過索引返回一條記錄即可。可根據B+樹索引的原理可知,對于一張大表,一般的B+樹需要2-3次磁盤IO。因此B+樹可以很好的完成操作,不需要分區的幫助。
參考