6 mysql底層解析——快取,Innodb_buffer_pool,包括連接、解析、快取、引擎、存儲等
- 2019 年 10 月 5 日
- 筆記
版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/tianyaleixiaowu/article/details/100574047
前面幾篇主要學習存儲,在磁碟上的存儲結構,內部格式等。
這一篇就來看記憶體,對於Innodb來說,也就是最關鍵的Innodb_buffer_pool。
我們都知道,記憶體讀寫和磁碟讀寫的速度不在一個數量級,在資料庫中,數據都是最終落到磁碟上的,想要達成快速的讀寫,必然要依靠快取技術。
Innodb的這個快取區就是Innodb_buffer_pool,當讀取數據時,就會先從快取中查看是否數據的頁(page)存在,不存在的話才去磁碟上檢索,查到後快取到這個pool里。同理,插入、修改、刪除也是先操作快取里數據,之後再以一定頻率更新到磁碟上。控制刷盤的機制,叫做Checkpoint。
Innodb_buffer_pool內部結構

注意,左邊那兩個不在Innodb_buffer_pool里,是另外一塊記憶體。只不過大部分的記憶體都屬於Innodb_buffer_pool的。
mysql安裝後,默認pool的大小是128M,可以通過show variables like 'innodb_buffer_pool%';命令查看。

可以通過show global status like '%innodb_buffer_pool_pages%'; 查看已經被佔用的和空閑的。共計8000多個page。

所以如果你的數據很多,而pool很小,那麼性能就好不了。
理論上來說,如果你給pool的記憶體足夠大,夠裝下所有數據,要訪問的所有數據都在pool里,那麼你的所有請求都是走記憶體,性能將是最好的,和redis類似。
官方建議pool的空間設置為物理記憶體的50%-75%。
在mysql5.7.5之後,可以在mysql不重啟的情況下動態修改pool的size,如果你設置的pool的size超過了1G的話,應該再修改一下Innodb_buffer_pool_instances=N,將pool分成N個pool實例,將來操作的數據會按照page的hash來映射到不同的pool實例。
這樣可以大幅優化多執行緒情況下,並發讀取同一個pool造成的鎖的競爭。
緩衝區LRU淘汰演算法
當pool的大小不夠用了,滿了,就會根據LRU演算法(最近最少使用)來淘汰老的頁面。最頻繁使用的頁在LRU列表的前端,最少使用的頁在LRU列表的尾端。淘汰的話,就首先釋放尾端的頁。
InnoDB的LRU和普通的不太一樣,Innodb的加入了midpoint位置的概念。最新讀取到的頁,並不是直接放到LRU列表的頭部的,而是放到midpoint位置。這個位置大概是LRU列表的5/8處,該參數由innodb_old_blocks_pct控制。

如37是默認值,表示新讀取的頁插入到LRU尾端37%的位置。在midpoint之後的列表都是old列表,之前的是new列表,可以簡單理解為new列表的頁都是最活躍的數據。
為什麼不直接放頭部?因為某些數據掃描操作需要訪問的頁很多,有時候這些頁僅僅在本次查詢有效,以後就不怎麼用了,並不算是活躍的熱點數據。那麼真正活躍的還是希望放到頭部去,這些新的暫不確定是否真正未來要活躍。所以,這可以理解為預熱。還引入了一個參數innodb_old_blocks_time用來表示頁讀取到mid位置後需要等待多久才會被加入到LRU列表的熱端。
還有一個重要的查詢命令可以看到這些資訊,show engine innodb status;

Database pages表示LRU列表中頁的數量,pages made young顯示了LRU列表中頁移動到前端的次數,Buffer pool hit rate表示緩衝池的命中率,100%表示良好,該值小於95%時,需要考慮是否因為全表掃描引起了LRU列表被污染。裡面還有其他的參數,可以自行查閱一下代表什麼意思。
Pool的主要空間
上面主要是講了insert buffer。對應的是增刪改時的快取處理。
從最上面的圖能看到,其實更多的、對性能影響更大的是讀快取。畢竟多數資料庫是讀多寫少。
讀快取主要數據是索引頁和數據頁,這個前面也說過,如果要讀取的數據在pool里沒有,那就去磁碟讀,讀到後的新頁放到pool的3/8位置,後續根據情況再決定是否放到LRU列表的頭部。注意,最小單位是頁,哪怕只讀一條數據,也會載入整個頁進去。如果是順序讀的話,剛好又在同一個頁里,譬如讀了id=1的,那麼再讀id=2的時,大概率直接從快取里讀。
插入緩衝insert buffer
從名字就能看出來是幹什麼的,它是buffer_pool的一部分,用來做insert操作時的快取的。
我們之前學習過b+ tree,也知道數據的存放格式,那麼當新插入數據時,倘若直接就插入到b+ tree里,那麼可想會多麼緩慢,需要讀取、找到要插入的地方,還要做樹的擴容、校驗、定址、落盤等等一大堆操作,等你插進去,姑娘都等老了。
在Innodb中,主鍵是行唯一標識,如果你的插入順序是按照主鍵遞增進行插入的,那麼還好,它不需要磁碟的隨機讀取,找到了頁,就能插,這樣速度還是可以的。
然而,如果你的表上有多個別的索引(二級索引),那麼當插入時,對於那個二級索引樹,就不是順序的了,它需要根據自己的索引列進行排序,這就需要隨機讀取了。二級索引越多,那麼插入就會越慢,因為要尋找的樹更多了。還有,如果你頻繁地更新同一條數據,倘若也頻繁地讀寫磁碟,那就不合適了,最好是將多個對同一page的操作,合併起來,統一操作。
所以,Innodb設計了Insert Buffer,對於非聚簇索引的插入、更新操作,不是每次都插入到索引頁中,而是先判斷該二級索引頁是否在緩衝池中,若在,就直接插入,若不在,則先插入一個insert buffer里,再以一定的頻率進行真正的插入到二級索引的操作,這時就可以聚合多個操作,一起去插入,就能提高性能。
然而,insert buffer需要同時滿足兩個條件時,才會被使用:
1 索引是二級索引
2 索引不是unique
注意,索引不能是unique,因為在插入緩衝時,資料庫並不去查詢索引頁來判斷插入的記錄的唯一性,如果查找了,就又會產生隨機讀取。
insert buffer的問題是,在寫密集的情況下,記憶體會佔有很大,默認最大可以佔用1/2的Innodb_buffer_pool的空間。很明顯,如果佔用過大,就會對其他的操作有影響,譬如能快取的查詢頁就變少了。可以通過IBUF_POOL_SIZE_PER_MAX_SIZE來進行控制。
變更緩衝change buffer
新版的Innodb引入了Change buffer,其實就是insert buffer類似的東西,只是把insert、update、delete都進行緩衝。
也就是所有DML操作,都會先進緩衝區,進行邏輯操作,後面才會真正落地。
通過參數Innodb_change_buffering開始查看修改各種buffer的選項。可選值有insertsdeletespurgeschangesallnone。


默認是所有操作都入buffer,右圖的參數是控制記憶體大小的,25代表最多使用1/4的緩衝池空間。
Insert Buffer原理
insert buffer的數據結構是一棵B+ tree,全局就一棵B+樹,負責對所有的表的二級索引進行插入快取。在磁碟上,該tree存放在共享表空間(希望還記得是什麼),默認ibdata1中。有時,通過獨立表空間的ibd文件試圖恢復表中數據時,可能會有CHECK TABLE錯誤,就是因為該表的二級索引中的數據可能還在insert buffer里,沒有刷新到自己的表空間。這時,可以通過repair table來重建表上的所有二級索引。
我們下面來看看這棵B+ tree里是什麼樣的。
首先,這裡存的值將來是要刷到二級索引的,我們至少要知道的資訊有:哪個表、表的哪個頁面(page)。
所以,insert buffer的b+ tree的內節點(非葉子節點)存放的是查詢的search key

space存的是哪個表,offset是所在頁的偏移量,可以理解為pageNo。
當發起了一次插入、更新時,首先判斷要操作的數據的頁(是二級索引的頁)是否已經在Innodb_buffer_pool里了,如果在,說明之前可能是查詢過該頁的數據,既然在快取了,那就不需要insert buffer了,直接去修改pool里該頁的數據即可。
如果不在,那就需要構造search key了,構造好,再加上被插入、修改的數據,插入到insert buffer的葉子節點裡去。
何時Merge insert buffer
insert buffer是一棵b+ tree,如果插入、修改的記錄的二級索引沒在pool里,就需要將記錄插到insert buffer。那麼什麼時候,insert buffer里的數據會被merge到真正的二級索引里去呢?
1 二級索引被讀取到pool時
2 insert buffer已無可用空間
3 master thread主執行緒後台刷入
第一種情況好理解,因為寫到insert buffer就是因為該記錄的二級索引頁不在pool里,現在如果被select到pool里去了,那麼自然就會直接merge過去。
第二種情況,那就是沒可用空間了,迫不得已,就得去刷磁碟了。
第三種,之前的文章還沒提到過,那就是有個master執行緒每秒或每10秒回進行一次merge insert buffer的操作,不同之處是每次merge的數量不同。
CheckPoint技術(redo log)
上面主要說了insert buffer,它是針對二級索引的插入、修改、刪除的快取,並且是數據頁不在pool里才用的。
那如果數據頁在pool里,發生了增刪改操作後,系統又是何時將數據落地刷入到磁碟呢?
你執行了一條DML語句,pool的頁就變成了臟頁,因為pool里的比磁碟里的新,兩者並不一致。資料庫就需要按一定規則將臟頁刷入到磁碟。
倘若每次一個頁發生變化就刷入磁碟,那開銷是非常大的,必須要攢夠一定數量或一段時間,再去刷入。還有,pool是記憶體,倘若還沒來得及刷入到磁碟,發生了宕機,那麼這些臟頁數據就會丟失。
為了解決上面的問題,當前事務資料庫系統普通採用了write ahead log策略,即當事務提交時,先寫重做日誌(redo log),再修改頁。當發生故障時,通過redo log來進行數據的恢復。
到這時,基本Innodb的增刪改查的流程,基本清晰了。
增刪改時,首先順序寫入redo log(順序寫磁碟,類似於kafka),然後修改pool頁(pool里沒有的,插入insert buffer),之後各種執行緒,會按照規則從快取里將數據刷入到磁碟,進行持久化,發生故障了,就從redo log恢復。
checkpoint(檢查點)做的事情就是:
1 緩衝池不夠用時,將臟頁刷新到磁碟
2 redo log不夠用時,刷新臟頁。
redo log也是磁碟文件,並不能無限增長,而且要循環利用。
Checkpoint所做的事情就是將緩衝池臟頁寫回磁碟,那麼主要就是每次刷多少,每次從哪裡去臟頁,什麼時間去刷的問題了。
目前有兩種Checkpoint:
1 資料庫關閉時,將所有臟頁刷新到磁碟,這是默認的方式。
2 Master Thread操作,這個主執行緒會每秒、每10秒從臟頁列表刷新一定比例的頁到磁碟,這是個非同步的操作,不會阻塞查詢。
3 LRU 列表空閑頁不足時,需要刷新一部分來自LRU列表的臟頁。
4 redo log文件不可用時,需要強制刷新一部分,為了保證redo log的循環利用。
5 pool空間不足時,臟頁太多,需要刷新。
兩次寫
這是Innodb的一個很獨特的功能,是用來保證數據頁的可靠性。
我仔細看過後,感覺設計很巧妙,但好像離我們比較遠了,所以就不寫了,想深入了解的可以自己去查查。