一、數據庫引擎
數據庫存儲引擎是數據庫底層軟件組織,是用於存儲、處理和保護數據的核心服務。利用數據庫引擎可控制訪問權限並快速處理數據,使用數據庫引擎可以創建用於存儲數據的表和用於查看、管理和保護數據安全的數據庫對象(如表,索引,視圖,圖表,缺省值,規則,觸發器,用戶,函數等)。可以使用DBMS管理數據庫對象。不同的存儲引擎提供不同的存儲機制、索引技巧、鎖定水平等功能,使用不同的存儲引擎,還可以獲得特定的功能。

從發展的歷史看,數據庫是數據管理的高級階段,它是由文件管理系統發展起的,由數據庫系統根據自己的數據庫引擎將存在磁盤上的文件讀出來變成各種各樣的數據和結構。數據庫在計算機中是以文件的形式存在的,下面簡單的說下文件存儲的格式:
- db.opt
MySQL的每個數據庫目錄中有一個文件db.opt文件,create database
時會自動生成一個文件db.opt。該文件主要用來存儲當前數據庫的默認字符集和字符校驗規則。
- *.ibd
.frm
文件中,后者存儲在
.ibd
文件中。.ibd 文件就是每一個表獨有的表空間,文件存儲了當前表的數據和相關的索引數據。
- *.frm
無論在 MySQL 中選擇了哪個存儲引擎,所有的 MySQL 表都會在硬盤上創建一個.frm
文件用來描述表的格式或者說定義;.frm
文件的格式在不同的平台上都是相同的。
二、MySql數據引擎分類
MYSQL支持多個數據庫引擎:ISAM、MYISAM、HEAP、INNODB和BERKLEY(BDB),要添加一個新的引擎,就必須重新編譯MYSQL。本來是對每一個數據引擎都詳細的介紹下,但是網上找了很久,都沒有詳細的文檔。這篇文章主要學習INNODB數據引擎,其他的數據引擎根據百科簡單的說下自己的理解。
1、ISAM
ISAM(發音是“阿塞姆”)搜索引擎的算法可參考淺談基礎算法之ISAM。ISAM數據庫引擎采用的算法是索引順序存取算法(ISAM),ISAM結構相當於多叉平衡樹,樹矮,能夠減少硬盤I/O次數,另外樹的節點記錄多,一次性讀取更多的數據,因此ISAM執行讀取操作的速度很快,而且不占用大量的內存和存儲資源。 ISAM是一個定義明確且歷經時間考驗的數據表格管理方法,它在設計之時就考慮到數據庫被查詢的次數要遠大於更新的次數,所以常用於讀取頻繁的應用中。ISAM的兩個主要不足之處在於,它不支持事務處理,也不能夠容錯:如果你的硬盤崩潰了,那么數據文件就無法恢復了。如果你正在把ISAM用在關鍵任務應用程序里,那就必須經常備份你所有的實時數據,通過其復制特性,MYSQL能夠支持這樣的備份應用程序。
2、MYISAM
MYISAM(發音為 "my-阿塞姆")是MYSQL的ISAM擴展格式。MYSQL-5.5版本之前默認引擎是MyISAM,之后是innoDB。MYISAM和ISAM一樣,讀取速度很快,除此之外還提供了索引、字段管理、表格鎖定等功能,對多個並發的讀寫操作進行了優化。所以大多數虛擬主機提供商和INTERNET平台提供商只允許使用MYISAM格式。但是它仍然沒有提供對數據庫事務的支持,也不支持行級鎖和外鍵,因此當INSERT或UPDATE數據時需要鎖定整個表,效率便會低一些。
3、HEAP(MEMORY)
HEAP(發音“黑普”)也叫Memory,因為它的數據是放在內存中的,訪問速度非常得快,比ISAM和MYISAM都快。HEAP表格在你需要使用SELECT表達式來選擇和操控數據的時候非常有用。Memory的表支持HASH索引和BTree索引,默認HASH索引。但是一旦服務關閉,表中的數據就會丟失掉,但是表的結構會保存下來,所以它所管理的數據是不穩定的。另外在數據行被刪除的時候,HEAP也不會浪費大量的空間。
4、BERKLEY
Berkeley DB (發音bɚkli)(BDB)是一個高性能的嵌入式數據庫引擎,它可以用來保存任意類型的鍵/值對,而且可以為一個鍵保存多個數據。Berkeley DB可以支持數千的並發線程同時操作數據庫,支持最大256TB的數據。具體可以參考官網文檔。
5、INNODB
從Mysql5.5.5版本開始,InnoDB是默認的表存儲引擎。InnoDB是事務安全的MySQL存儲引擎,支持ACID事務。其設計目標主要面向在線事務處理(OLTP)的應用。其特點是行鎖設計、支持外鍵,並支持非鎖定讀,即默認讀操作不會產生鎖。InnoDB存儲數據是基於磁盤存儲的,且其記錄是按照頁的方式進行管理。
三、INNODB體系結構
如下圖所示,InnoDB存儲引擎由緩沖池和一些后台線程組成,InnoDB存儲引擎有多個內存塊,這些內存塊組成了一個大的緩沖池,該緩沖池可以被運行的后台線程所共享。后台線程主要負責刷新緩沖池中的數據、將已修改的數據刷新到磁盤等等。為什么要是用緩沖池?還要后台線程進行刷新到磁盤的操作?InnoDB 存儲引擎是基於磁盤存儲的,即數據都是存儲在磁盤上的,由於 CPU 速度和磁盤速度之間的鴻溝,InnoDB 引擎使用緩沖池技術來提高數據庫的整體性能,即通過內存的速度來彌補磁盤速度慢對數據庫性能造成的影響。因此緩沖池主要工作
-
維護所有進程/線程需要訪問的多個內部數據結構
-
緩存磁盤上的數據,方便快速讀取,同時在對磁盤文件修改之前進行緩存
-
緩存重做日志(redo log)
后台線程主要工作
-
刷新緩沖池中的數據,保證緩沖池中緩存的數據最新
-
將已修改數據文件刷新到磁盤文件
-
保證數據庫異常時InnoDB能恢復到正常運行狀態

mysql> show engine innodb status; +--------+------+-----------------------------------------------+ | Type | Name | Status | +--------+------+-----------------------------------------------+ | InnoDB | | #第一段是頭部信息,它僅僅聲明了輸出的開始,其內容包括當前的日期和時間,以及自上次輸出以來經過的時長。
===================================== 2020-12-04 15:24:15 0x90e8 INNODB MONITOR OUTPUT #當前日期和時間 =====================================
#顯示的是計算出這一平均值的時間間隔,即自上次輸出以來的時間,
或者是距上次內部復位的時長#從innodb1.0.x開始,可以使用命令show engine innodb status;
來查看master thread的狀態信息 Per second averages calculated from the last 49 seconds ----------------- BACKGROUND THREAD #后台進程 -----------------
#主線程每秒loop循環的次數(9 激活的次數 0 停止的次數 358242 等待的次數)
srv_master_thread loops: 9 srv_active, 0 srv_shutdown, 358242 srv_idle srv_master_thread log flush and writes: 0 ---------- SEMAPHORES #鎖信息 ----------
#操作系統等待數組的信息,它是一個插槽數組,innodb在數組里為信號量保留了一些插槽,
操作系統用這些信號量給線程發送信號,使線程可以繼續運行。reservation count顯示了innodb分配插槽的頻度
OS WAIT ARRAY INFO: reservation count 2
#操作系統等待線程,signal count表示線程通過數組得到信號的頻度,數值越大,
表示有很多I/0等待或者是存在InnoDB爭用問題 OS WAIT ARRAY INFO: signal count 2
RW-shared spins 0, rounds 0, OS waits 0 #RW-shared 共享鎖,spins表示共享讀鎖期間讀寫鎖等待的個數,
rounds表示循環迭帶的個數,OS waits系統調用的等待個數
RW-excl spins 0, rounds 0, OS waits 0 #RW-excl 排他鎖,spins表示排它寫鎖期間讀寫鎖等待的個數,
rounds表示循環迭帶的個數,OS waits系統調用的等待個數
RW-sx spins 0, rounds 0, OS waits 0 Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx ------------ TRANSACTIONS #事務信息 ------------ Trx id counter 37797 #下一個事務號
#所有編號小於37724的事務都已經從歷史記錄列表中清除了
Purge done for trx's n:o < 37724 undo n:o < 0 state: running but idle
History list length 77 #歷史列表的長度 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 283697766612480, not started 0 lock struct(s), heap size 1136, 0 row lock(s) ---TRANSACTION 283697766611608, not started 0 lock struct(s), heap size 1136, 0 row lock(s) ---TRANSACTION 283697766610736, not started 0 lock struct(s), heap size 1136, 0 row lock(s) -------- FILE I/O #文件IO -------- I/O thread 0 state: wait Windows aio (insert buffer thread) #插入緩沖線程 I/O thread 1 state: wait Windows aio (log thread) #日志線程 I/O thread 2 state: wait Windows aio (read thread) #4個異步讀線程 I/O thread 3 state: wait Windows aio (read thread) I/O thread 4 state: wait Windows aio (read thread) I/O thread 5 state: wait Windows aio (read thread) I/O thread 6 state: wait Windows aio (write thread) #4個異步寫線程 I/O thread 7 state: wait Windows aio (write thread) I/O thread 8 state: wait Windows aio (write thread) I/O thread 9 state: wait Windows aio (write thread) Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] , #每個讀寫線程狀態(4個讀4個寫) ibuf aio reads:, log i/o's:, sync i/o's: #插入緩沖線程的狀態,每秒日志IO狀態,IO同步的狀態 Pending flushes (fsync) log: 0; buffer pool: 0 #文件同步的日志狀態,緩沖池的個數 1062 OS file reads, 582 OS file writes, 167 OS fsyncs #讀了多少個文件,系統寫了多少個文件,系統同步了多少個文件 0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s #秒讀的信息,平均每秒讀的字節數,每秒寫入的信息,每秒同步的信息 ------------------------------------- INSERT BUFFER AND ADAPTIVE HASH INDEX #插入緩沖和自適應哈稀索引 -------------------------------------
#size表示緩沖索引樹的當前大小,free表示空閑列表的長度,seg size文件段中已分配段的個數,merges合並頁的個數
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations: insert 0, delete mark 0, delete 0 #insert插入緩沖的次數,delete mark標記為已刪除的次數,delete表示purge的次數(刪除) discarded operations: insert 0, delete mark 0, delete 0 #有多少個insert buffer被丟棄,有多少個insert buffer被標記為已刪除,purge多少個insert buffer等 Hash table size 34679, node heap has 0 buffer(s) #自適應哈稀索引單元格的數量與預留緩沖結構的數量 Hash table size 34679, node heap has 1 buffer(s) Hash table size 34679, node heap has 0 buffer(s) Hash table size 34679, node heap has 0 buffer(s) Hash table size 34679, node heap has 0 buffer(s) Hash table size 34679, node heap has 0 buffer(s) Hash table size 34679, node heap has 1 buffer(s) Hash table size 34679, node heap has 3 buffer(s) 0.00 hash searches/s, 0.00 non-hash searches/s #使用哈稀索引的數量與不能使用哈稀索引時向下搜索B樹索引的次數 --- LOG #重做日志信息 --- Log sequence number 32459336 #當前重做日志序列號 Log buffer assigned up to 32459336 Log buffer completed up to 32459336 Log written up to 32459336 Log flushed up to 32459336 #刷新到重做日志文件的序列號 Added dirty pages up to 32459336 Pages flushed up to 32459336 #刷新到磁盤的日志序列號 Last checkpoint at 32459336 #下一個日志序列號 174 log i/o's done, 0.00 log i/o's/second #innodb啟動后的io個數,最近一次顯示之后的每秒io操作個數 ---------------------- BUFFER POOL AND MEMORY #緩存池和內存 ---------------------- Total large memory allocated 137363456 #分配的最大內存總數 Dictionary memory allocated 446405 #數據字典占用的字節數 Buffer pool size 8192 #緩沖池的個數 Free buffers 6994 #剩余緩沖區的個數 Database pages 1193 #LRU中數據頁的個數 Old database pages 460 #LRU中舊數據頁的個數 Modified db pages 0 #LRU中已修改的數據頁個數 Pending reads 0 #掛起讀操作的個數
#通過使用LRU算法等待刷新的頁數,在BUF_FLUSH_LIST列表等待刷新的頁數,在BUF_FLUSH_SINGLE_PAGE等待刷新的頁數
Pending writes: LRU 0, flush list 0, single page 0 Pages made young 1, not young 0 #第一次訪問變成新頁面的次數,0沒有變成新頁面的次數 0.00 youngs/s, 0.00 non-youngs/s #LRU中每秒變成新頁面的速率,沒有變成新頁面的速率 Pages read 1039, created 154, written 363 #1039讀操作的頁面個數,154在緩沖池中創建了沒有讀取的頁面個數,363寫操作的頁面個數 0.00 reads/s, 0.00 creates/s, 0.00 writes/s #LRU中每秒讀取數據的頁速率,每秒創建數據的頁速率,每秒寫入數據的頁速率
#讀取頁面數與獲得緩沖池頁面的比例,變為新頁面的頁面數與獲得緩沖池頁面的比例,沒有變為新頁面的頁面數與獲得緩沖池頁面的比例
No buffer pool page gets since the last printout Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s #預讀的速率,不通過訪問剔除的預讀頁面的個數 LRU len: 1193, unzip_LRU len: 0 #LRU列表長度,unzip_LRU列表長度 I/O sum[0]:cur[0], unzip sum[0]:cur[0] #LRU在LRU列表中I/O操作的次數,unzip在LRU列表中I/O操作的次數 -------------- ROW OPERATIONS #行操作 -------------- 0 queries inside InnoDB, 0 queries in queue #有多少個正在查詢操作個數 0 read views open inside InnoDB #顯示只讀視圖的數量 Process ID=7264, Main thread ID=00000000000028AC , state=sleeping #顯示主線程的ID及其狀態 Number of rows inserted 14, updated 395, deleted 0, read 6717 #從innodb存儲引擎啟動后插入,更新,刪除,查詢的行數 0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s #最近一次顯示增,刪,改,查的速率 ---------------------------- END OF INNODB MONITOR OUTPUT ============================ | +--------+------+---------------------------------------------------------------------------------+ 1 row in set
1、后台線程
MySQL是一個單進程多線程架構的數據庫,也就是說MySQL數據庫實例在系統中表現形式就是一個進程。InnoDB是多線程的模型,后台有多個不同的線程,用來負責不同的任務。主要有如下:
(1)Master Thread
這是最核心的一個線程,主要負責將緩沖池中的數據異步刷新到磁盤,保證數據的一致性,包括贓頁的刷新、合並插入緩沖、UNDO 頁的回收等。Master thread在主循環中,分為兩類操作,每秒鍾的操作和每10秒鍾的操作:-
每秒一次的操作
-
日志緩沖刷新到磁盤: 即使這個事務還沒有提交(總是),這點解釋了為什么再大的事務commit時都很快;
-
合並插入緩沖(可能): 合並插入並不是每秒都發生,InnoDB會判斷當前一秒內發生的IO次數是否小於5,如果是,則系統認為當前的IO壓力很小,可以執行合並插入緩沖的操作。
-
至多刷新100個InnoDB的緩沖池的臟頁到磁盤(可能) : 這個刷新100個臟頁也不是每秒都在做,InnoDB引擎通過判斷當前緩沖池中臟頁的比例(buf_get_modified_ratio_pct)是否超過了配置文件中innodb_max_drity_pages_pct參數(默認是90,即90%),如果超過了這個閾值,InnoDB引擎認為需要做磁盤同步操作,將100個臟頁寫入磁盤。
-
每10秒一次的操作
-
刷新100個臟頁到磁盤(可能): InnoDB引擎先判斷過去10秒內磁盤的IO操作是否小於200次,如果是,認為當前磁盤有足夠的IO操作能力,即將100個臟頁刷新到磁盤。
-
合並至多5個插入緩沖(總是): 此次的合並插入緩沖操作總會執行,不同於每秒操作時可能發生的合並操作。
-
將日志緩沖刷新到磁盤(總是): InnoDB引擎會再次執行日志緩沖刷新到磁盤的操作,與每秒發生的操作一樣。
-
刪除無用的undo頁(總是): 當對表執行update,delete操作時,原先的行會被標記為刪除,但是為了一致性讀的關系,需保留這些行版本的信息,在進行10S一次的刪除操作時,InnoDB引擎會判斷當前事務系統中已被刪除的行是否可以刪除,如果可以,InnoDB會立即將其刪除。InnoDB每次最多刪除20個Undo頁。
-
產生一個檢查點(checkpoing);
(2)IO Thread
在 InnoDB 存儲引擎中大量使用了異步 IO 來處理寫 IO 請求,IO Thread 的工作主要是負責這些 IO 請求的回調處理。通過以下命令查詢IO線程:
mysql> show variables like 'innodb_%io_threads';

(3)Purge Thread
事務被提交之后,undo log 可能不再需要,因此需要 Purge來回收已經使用並分配的 undo頁,purge操作默認是由master thread中完成的,為了減輕master thread的工作,提高cpu使用率以及提升存儲引擎的性能,用戶可以在參數文件中添加如下命令來啟動獨立的purge thread
innodb_purge_threads=1
show variables like 'innodb_purge%'
其中innodb_purge_threads 為開啟的Purge Thread數量,innodb_purge_batch_size為設置每次purge清理的undo頁數,如果值太大則會導致CPU和磁盤IO過於集中。
(4)Page Cleaner Thread
Page Cleaner Thread 是在InnoDB 1.2.x版本新引入的,其作用是將之前版本中臟頁的刷新操作都放入單獨的線程中來完成,這樣減輕了 Master Thread 的工作及對於用戶查詢線程的阻塞。
2、緩沖池及其組成
InnoDB 存儲引擎是基於磁盤存儲的,即數據都是存儲在磁盤上的,由於 CPU 速度和磁盤速度之間的鴻溝,InnoDB 引擎使用緩沖池技術來提高數據庫的整體性能,即通過內存的速度來彌補磁盤速度慢對數據庫性能造成的影響。其工作方式總是將數據庫文件按頁(每頁16K)讀取到緩沖池,然后按最近最少使用(LRU)的算法來保留在緩沖池中的緩存數據。在數據庫中進行讀操作時,首先將從磁盤讀到的頁存放在緩沖池中,下一次讀取相同的頁時,首先判定是否存在緩沖池中,如果有就是被命中直接讀取,沒有的話就從磁盤中讀取。在數據庫進行改操作時,首先修改緩沖池中的頁(修改后,該頁即為臟頁),然后在以一定的頻率刷新到磁盤上。這里的刷新機制不是每頁在發生變更時觸發。而是通過一種checkpoint機制刷新到磁盤的。所以緩沖池的大小直接影響着數據庫的整體性能,可以通過配置參數innodb_buffer_pool_size來設置。從架構圖中可以看出緩沖池中緩存的數據頁類型有索引頁、數據頁、 undo 頁、插入緩存、自適應哈希索引、 InnoDB 的鎖信息、數據字典信息等。索引頁和數據頁占緩沖池的很大一部分。
- 數據頁和索引頁: Page是Innodb存儲的最基本結構,也是Innodb磁盤管理的最小單位,與數據庫相關的所有內容都存儲在Page結構里。Page分為幾種類型,數據頁和索引頁就是其中最為重要的兩種類型。
-
插入緩存: 在InnoDB引擎上進行插入操作時,一般需要按照主鍵順序進行插入,這樣才能獲得較高的插入性能。當一張表中存在非聚簇的且不唯一的索引時,在插入時,數據頁的存放還是按照主鍵進行順序存放,但是對於非聚簇索引葉節點的插入不再是順序的了,這時就需要離散的訪問非聚簇索引頁,由於隨機讀取的存在導致插入操作性能下降。InnoDB為此設計了Insert Buffer來進行插入優化。對於非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引頁中,而是先判斷插入的非聚集索引是否在緩沖池中,若在,則直接插入;若不在,則先放入到一個Insert Buffer中。看似數據庫這個非聚集的索引已經查到葉節點,而實際沒有,這時存放在另外一個位置。然后再以一定的頻率和情況進行Insert Buffer和非聚簇索引頁子節點的合並操作。這時通常能夠將多個插入合並到一個操作中,這樣就大大提高了對於非聚簇索引的插入性能。
-
自適應哈希索引: InnoDB會根據訪問的頻率和模式,為熱點頁建立哈希索引,來提高查詢效率。InnoDB存儲引擎會監控對表上各個索引頁的查詢,如果觀察到建立哈希索引可以帶來速度上的提升,則建立哈希索引,所以叫做自適應哈希索引。自適應哈希索引是通過緩沖池的B+樹頁構建而來,因此建立速度很快,而且不需要對整張數據表建立哈希索引。其有一個要求,即對這個頁的連續訪問模式必須是一樣的,也就是說其查詢的條件(WHERE)必須完全一樣,而且必須是連續的。
-
鎖信息 : InnoDB存儲引擎會在行級別上對表數據進行上鎖。不過InnoDB也會在數據庫內部其他很多地方使用鎖,從而允許對多種不同資源提供並發訪問。數據庫系統使用鎖是為了支持對共享資源進行並發訪問,提供數據的完整性和一致性。
- 數據字典信息 : InnoDB有自己的表緩存,可以稱為表定義緩存或者數據字典。當InnoDB打開一張表,就增加一個對應的對象到數據字典。數據字典是對數據庫中的數據、庫對象、表對象等的元信息的集合。在MySQL中,數據字典信息內容就包括表結構、數據庫名或表名、字段的數據類型、視圖、索引、表字段信息、存儲過程、觸發器等內容。MySQL INFORMATION_SCHEMA庫提供了對數據局元數據、統計信息、以及有關MySQL server的訪問信息(例如:數據庫名或表名,字段的數據類型和訪問權限等)。該庫中保存的信息也可以稱為MySQL的數據字典。
- 重做日志緩沖:InnoDB有buffer pool(簡稱bp)。bp是數據庫頁面的緩存,對InnoDB的任何修改操作都會首先在bp的page上進行,然后這樣的頁面將被標記為dirty並被放到專門的flush list上,后續將由master thread或專門的刷臟線程階段性的將這些頁面寫入磁盤。這樣的好處是避免每次寫操作都操作磁盤導致大量的隨機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操作。因此,磁盤的性能在一定程度上也決定了事務提交的性能。InnoDB 存儲引擎先將重做日志信息放入這個緩沖區,然后以一定頻率將其刷新到重做日志文件。重做日志文件一般不需要設置得很大,因為在下列三種情況下重做日志緩沖中的內容會刷新到磁盤的重做日志文件中。額外的緩沖池
- Master Thread 每一秒將重做日志緩沖刷新到重做日志文件
- 每個事物提交時會將重做日志緩沖刷新到重做日志文件
- 當重做日志緩沖剩余空間小於1/2時,重做日志緩沖刷新到重做日志文件
- 額外的緩沖池:在 InnoDB 存儲引擎中,對一些數據結構本身的內存進行分配時,需要從額外的內存池中進行申請。例如: 分配了緩沖池,但是每個緩沖池中的幀緩沖還有對應的緩沖控制對象,這些對象記錄以一些諸如 LRU, 鎖,等待等信息,而這個對象的內存需要從額外的內存池中申請。
3、緩沖池的原理
上邊已經介紹了緩沖池的概念以及緩沖池中的組成,這里介紹下緩沖池工作原理。
(1)怎么將磁盤上的頁緩存到內存中的Buffer Pool中呢?
Buffer Pool其實是一片連續的內存空間,那么怎么將磁盤上的頁緩存到內存中的Buffer Pool中呢?直接把需要緩存的頁向Buffer Pool里一個一個往里懟么?不不不,為了更好的管理這些被緩存的頁,InnoDB為每一個緩存頁都創建了一些所謂的控制信息,這些控制信息包括該頁所屬的表空間編號、頁號、頁在Buffer Pool中的地址、一些鎖信息以及LSN信息,當然還有一些別的控制信息。
每個緩存頁對應的控制信息占用的內存大小是相同的,我們就把每個頁對應的控制信息占用的一塊內存稱為一個控制塊,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 后邊,所以整個Buffer Pool對應的內存空間看起來就是這樣的:

(2)InnoDB存儲引擎是怎么對緩沖池進行管理的?
空閑鏈表:
當我們最初啟動MySQL服務器的時候,需要完成對Buffer Pool的初始化過程,就是分配Buffer Pool的內存空間,把它划分成若干對控制塊和緩存頁。但是此時並沒有真實的磁盤頁被緩存到Buffer Pool中(因為還沒有用到),之后隨着程序的運行,會不斷的有磁盤上的頁被緩存到Buffer Pool中,那么問題來了,從磁盤上讀取一個頁到Buffer Pool中的時候該放到哪個緩存頁的位置呢?或者說怎么區分Buffer Pool中哪些緩存頁是空閑的,哪些已經被使用了呢?我們最好在某個地方記錄一下哪些頁是可用的,我們可以把所有空閑的頁包裝成一個節點組成一個鏈表,這個鏈表也可以被稱作Free鏈表(或者說空閑鏈表)。因為剛剛完成初始化的Buffer Pool中所有的緩存頁都是空閑的,所以每一個緩存頁都會被加入到Free鏈表中,假設該Buffer Pool中可容納的緩存頁數量為n,那增加了Free鏈表的效果圖就是這樣的:

從圖中可以看出,我們為了管理好這個Free鏈表,特意為這個鏈表定義了一個控制信息,里邊兒包含着鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。我們在每個Free鏈表的節點中都記錄了某個緩存頁控制塊的地址,而每個緩存頁控制塊都記錄着對應的緩存頁地址,所以相當於每個Free鏈表節點都對應一個空閑的緩存頁。有了這個Free鏈表事兒就好辦了,每當需要從磁盤中加載一個頁到Buffer Pool中時,就從Free鏈表中取一個空閑的緩存頁,並且把該緩存頁對應的控制塊的信息填上,然后把該緩存頁對應的Free鏈表節點從鏈表中移除,表示該緩存頁已經被使用了~
上面介紹了緩存池中的空閑鏈表,free list用來維護buffer pool的空閑緩存頁的數據結構。
內存空間不足引起的問題?
簡單地回顧Buffer Pool的工作機制。Buffer Pool兩個最主要的功能:
- 一個是加速讀,加速讀呢? 就是當需要訪問一個數據頁面的時候,如果這個頁面已經在緩存池中,那么就不再需要訪問磁盤,直接從緩沖池中就能獲取這個頁面的內容。
- 一個是加速寫,加速寫呢?就是當需要修改一個頁面的時候,先將這個頁面在緩沖池中進行修改,記下相關的重做日志,這個頁面的修改就算已經完成了。至於這個被修改的頁面什么時候真正刷新到磁盤,這個是后台刷新線程來完成的。
在實現上面兩個功能的同時,需要考慮客觀條件的限制,因為機器的內存大小是有限的,所以MySQL的InnoDB Buffer Pool的大小同樣是有限的,如果需要緩存的頁占用的內存大小超過了Buffer Pool大小,也就是Free鏈表中已經沒有多余的空閑緩存頁的時候豈不是很尷尬,發生了這樣的事兒該咋辦?當然是把某些舊的緩存頁從Buffer Pool中移除,然后再把新的頁放進來嘍~ 那么問題來了,移除哪些緩存頁呢?
LRU list
為了回答上邊的問題,我們還需要回到我們設立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列表的熱端。可以通過設置該參數保證熱點數據不輕易被刷出。
FLUSH鏈表
前面我們講到頁面更新是在緩存池中先進行的,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱為臟頁(英文名: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列表中,也存在與Flush列表中。LRU列表用來管理緩沖池中頁的可用性,Flush列表用來管理將頁刷新回磁盤,二者互不影響。
這三個重要列表(LRU list, free list,flush list)的關系可以用下圖表示:

Free鏈表跟LRU鏈表的關系是相互流通的,頁在這兩個鏈表間來回置換。而FLUSH鏈表記錄了臟頁數據,是通過指針指向了LRU鏈表,所以圖中FLUSH鏈表被LRU鏈表包裹。
4、INNODB重要特性
(1) 插入緩沖
一般情況下,主鍵是行唯一的標識符,通常應用程序中行記錄的插入順序是按照主鍵遞增的順序進行插入的,因此,插入聚集索引一般是順序的,不需要磁盤的隨機讀取。因為,對於此類情況下的插入,速度還是非常快的。(如果主鍵類是UUID這樣的類,那么插入和輔助索引一樣,也是隨機的。)
如果索引是非聚集的且不唯一,在進行插入操作時,數據的存放對於非聚集索引葉子節點的插入不是順序的,這時需要離散地訪問非聚集索引頁,由於隨機讀取的存在而導致了插入操作性能下降。這是因為B+樹的特性決定了非聚集索引插入的離散性。
Insert Buffer的設計,對於非聚集索引的插入和更新操作,不是每一次直接插入到索引頁中,而是先判斷插入非聚集索引頁是否在緩沖池中,若存在,則直接插入,不存在,則先放入一個Insert Buffer對象中。數據庫這個非聚集的索引已經插到葉子節點,而實際並沒有,只是存放在另一個位置。然后再以一定的頻率和情況進行Insert Buffer和輔助索引頁子節點的merge(合並)操作,這時通常能將多個插入合並到一個操作中(因為在一個索引頁中),這就大大提高了對於非聚集索引插入的性能。
需要滿足的兩個條件:
- 索引是輔助索引;
- 索引不是唯一的。
輔助索引不能是唯一的,因為在插入緩沖時,數據庫並不去查找索引頁來判斷插入的記錄的唯一性。如果去查找肯定又會有離散讀取的情況發生,從而導致Insert Buffer失去了意義。
(2)兩次寫
如果說插入緩沖是為了提高寫性能的話,那么兩次寫是為了提高可靠性。介紹兩次寫之前,說一下部分寫失效:
場景:當數據庫正在從內存向磁盤寫一個數據頁時,數據庫宕機,從而導致這個頁只寫了部分數據,這就是部分寫失效,它會導致數據丟失。這時是無法通過重做日志恢復的,因為重做日志記錄的是對頁的物理修改,如果頁本身已經損壞,重做日志也無能為力。
從上面分析我們知道,在部分寫失效的情況下,我們在應用重做日志之前,需要原始頁的一個副本,兩次寫就是為了解決這個問題,下面是它的原理圖:

- 內存中的兩次寫緩沖(doublewrite buffer),大小為2MB
- 磁盤上共享表空間中連續的128頁,大小也為2MB
其原理是這樣的:
1)當刷新緩沖池臟頁時,並不直接寫到數據文件中,而是先拷貝至內存中的兩次寫緩沖區。
2)接着從兩次寫緩沖區分兩次寫入磁盤共享表空間中,每次寫入1MB
3)待第2步完成后,再將兩次寫緩沖區寫入數據文件
這樣就可以解決上文提到的部分寫失效的問題,因為在磁盤共享表空間中已有數據頁副本拷貝,如果數據庫在頁寫入數據文件的過程中宕機,在實例恢復時,可以從共享表空間中找到該頁副本,將其拷貝覆蓋原有的數據頁,再應用重做日志即可。其中第2步是額外的性能開銷,但由於磁盤共享表空間是連續的,因此開銷不是很大。可以通過參數skip_innodb_doublewrite禁用兩次寫功能,默認是開啟的,強烈建議開啟該功能。
(3) 自適應哈希索引
哈希是一種非常快的查找方法,在一般情況時間復雜度為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后,讀取和寫入的速度可以提高2倍,負責索引的鏈接操作性能可以提高5倍。其設計思想是數據庫自由化的,無需DBA對數據庫進行人為調整。
(4)異步IO
為了提高磁盤操作性能,當前的數據庫系統都采用異步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需要操作系統提供支持。MySQL可以通過參數innodb_use_native_aio來決定是否啟用Native AIO。在InnoDB存儲引擎中,read ahead方式的讀取都是通過AIO完成,臟頁的刷新,也是通過AIO完成。
(5)刷新領接頁
當刷新一個臟頁時,innodb會檢測該頁所在區(extent)的所有頁,如果是臟頁,那么一起進行刷新。這樣做,通過AIO將多個IO寫入操作合並為一個IO操作。該工作機制在傳統機械磁盤下有顯著優勢。但是需要考慮下面兩個問題:
- 是不是將不怎么臟的頁進行寫入,而該頁之后又會很快變成臟頁?
- 固態硬盤有很高IOPS,是否還需要這個特性?
為此InnoDB存儲引擎1.2.x版本開始提供參數innodb_flush_neighbors來決定是否啟用。對於傳統機械硬盤建議使用,而對於固態硬盤可以關閉。
5、CheckPoint技術
CheckPoint技術是用來解決如下幾個問題:
-
縮短數據庫恢復時間:縮短數據庫恢復時間,重做日志中記錄了的checkpoint的位置,這個點之前的頁已經刷新回磁盤,只需要對checkpoint之后的重做日志進行恢復。這樣就大大縮短了恢復時間
-
緩沖池不夠用時,將臟頁刷新到磁盤:緩沖池不夠用時,根據LRU算法,溢出最近最少使用的頁,如果頁為臟頁,強制執行checkpoint,將臟頁刷新回磁盤。
-
重做日志不可用時,刷新臟頁:重做日志不可用,是指重做日志的這部分不可以被覆蓋,因為由於重做日志的設計是循環使用的,這部分對應的數據還未刷新到磁盤上。數據庫恢復時,如果不需要這部分日志即可被覆蓋;如果需要必須強制執行checkpoint將緩沖池中的頁至少刷新到當前重做日志的位置。
InnoDB存儲引擎內部,兩種checkpoint,分別為:
-
Sharp Checkpoint
-
Fuzzy Checkpoint
什么時間觸發checkpoint?
(1)Sharp Checkpoint發生在數據庫關閉時,將所有的臟頁都刷新回磁盤,這是默認的工作方式,即參數:innodb_fast_shutdown=1。不適用於數據庫運行時的刷新。
(2)在數據庫運行時,InnoDB存儲引擎內部采用Fuzzy Checkpoint,只刷新一部分臟頁。
幾種發生Fuzzy Checkpoint的情況?
(1)MasterThread Checkpoint
異步刷新,每秒或每10秒從緩沖池臟頁列表刷新一定比例的頁回磁盤。
(2)FLUSH_LRU_LIST Checkpoint
InnoDB存儲引擎需要保證LRU列表中差不多有100個空閑頁可供使用。在InnoDB 1.2.x版本之前,用戶查詢線程會檢查LRU列表是否有足夠的空間操作。如果沒有,根據LRU算法,溢出LRU列表尾端的頁,如果這些頁有臟頁,需要進行checkpoint。因此叫:flush_lru_list checkpoint。InnoDB 1.2.x開始,這個檢查放在了單獨的進程(Page Cleaner)中進行。
設置參數:innodb_lru_scan_dept:控制LRU列表中可用頁的數量,該值默認1024
(3)Async/Sync Flush Checkpoint
指重做日志不可用的情況,需要強制刷新頁回磁盤。
即臟頁太多,強制checkpoint.保證緩沖池有足夠可用的頁。參數設置:innodb_max_dirty_pages_pct = 75 表示:當緩沖池中臟頁的數量占75%時,強制checkpoint。
四、MyISAM 和 InnoBD如何選擇
- MyISAM不支持事務,讀取速度快,支持全文搜索,如果應用中需要執行大量的SELECT查詢,那么MyISAM是更好的選擇。
- InnoDB支持ACID事務,如果有頻繁的插入、更新操作,並發量大,則應該使用InnoDB