應用系統分層架構,爲了加速數據訪問,會把最常訪問的數據,放在緩存(cache)裏,避免每次都去訪問數據庫。算法
操做系統,會有緩衝池(buffer pool)機制,避免每次訪問磁盤,以加速數據的訪問。shell
MySQL做爲一個存儲系統,一樣具備緩衝池(buffer pool)機制,以免每次查詢數據都進行磁盤IO。數據庫
今天,和你們聊一聊InnoDB的緩衝池。緩存
緩存表數據與索引數據,把磁盤上的數據加載到緩衝池,避免每次訪問都進行磁盤IO,起到加速訪問的做用。架構
速度快,那爲啥不把全部數據都放到緩衝池裏?
凡事都具有兩面性,拋開數據易失性不說,訪問快速的反面是存儲容量小:
(1)緩存訪問快,但容量小,數據庫存儲了200G數據,緩存容量可能只有64G;
(2)內存訪問快,但容量小,買一臺筆記本磁盤有2T,內存可能只有16G;
所以,只能把「最熱」的數據放到「最近」的地方,以「最大限度」的下降磁盤訪問。ide
在介紹具體細節以前,先介紹下「預讀」的概念。工具
磁盤讀寫,並非按需讀取,而是按頁讀取,一次至少讀一頁數據(通常是4K),若是將來要讀取的數據就在頁中,就可以省去後續的磁盤IO,提升效率。性能
數據訪問,一般都遵循「集中讀寫」的原則,使用一些數據,大機率會使用附近的數據,這就是所謂的「局部性原理」,它代表提早加載是有效的,確實可以減小磁盤IO。優化
(1)磁盤訪問按頁讀取可以提升性能,因此緩衝池通常也是按頁緩存數據;
(2)預讀機制啓示了咱們,能把一些「可能要訪問」的頁提早加入緩衝池,避免將來的磁盤IO操做;操作系統
最容易想到的,就是LRU(Least recently used)。
畫外音:memcache,OS都會用LRU來進行頁置換管理,但MySQL的玩法並不同。
最多見的玩法是,把入緩衝池的頁放到LRU的頭部,做爲最近訪問的元素,從而最晚被淘汰。這裏又分兩種狀況:
(1)頁已經在緩衝池裏,那就只作「移至」LRU頭部的動做,而沒有頁被淘汰;
(2)頁不在緩衝池裏,除了作「放入」LRU頭部的動做,還要作「淘汰」LRU尾部頁的動做;
如上圖,假如管理緩衝池的LRU長度爲10,緩衝了頁號爲1,3,5…,40,7的頁。
假如,接下來要訪問的數據在頁號爲4的頁中:
(1)頁號爲4的頁,原本就在緩衝池裏;
(2)把頁號爲4的頁,放到LRU的頭部便可,沒有頁被淘汰;
畫外音:爲了減小數據移動,LRU通常用鏈表實現。
假如,再接下來要訪問的數據在頁號爲50的頁中:
(1)頁號爲50的頁,原來不在緩衝池裏;
(2)把頁號爲50的頁,放到LRU頭部,同時淘汰尾部頁號爲7的頁;
傳統的LRU緩衝池算法十分直觀,OS,memcache等不少軟件都在用,MySQL爲啥這麼矯情,不能直接用呢?
這裏有兩個問題:
(1)預讀失效;
(2)緩衝池污染;
因爲預讀(Read-Ahead),提早把頁放入了緩衝池,但最終MySQL並無從頁中讀取數據,稱爲預讀失效。
要優化預讀失效,思路是:
(1)讓預讀失敗的頁,停留在緩衝池LRU裏的時間儘量短;
(2)讓真正被讀取的頁,才挪到緩衝池LRU的頭部;
以保證,真正被讀取的熱數據留在緩衝池裏的時間儘量長。
具體方法是:
(1)將LRU分爲兩個部分:
舉個例子,整個緩衝池LRU如上圖:
(1)整個LRU長度是10;
(2)前70%是新生代;
(3)後30%是老生代;
(4)新老生代首尾相連;
假若有一個頁號爲50的新頁被預讀加入緩衝池:
(1)50只會從老生代頭部插入,老生代尾部(也是總體尾部)的頁會被淘汰掉;
(2)假設50這一頁不會被真正讀取,即預讀失敗,它將比新生代的數據更早淘汰出緩衝池;
假如50這一頁馬上被讀取到,例如SQL訪問了頁內的行row數據:
(1)它會被馬上加入到新生代的頭部;
(2)新生代的頁會被擠到老生代,此時並不會有頁面被真正淘汰;
改進版緩衝池LRU可以很好的解決「預讀失敗」的問題。
畫外音:但也不要因噎廢食,由於懼怕預讀失敗而取消預讀策略,大部分狀況下,局部性原理是成立的,預讀是有效的。
新老生代改進版LRU仍然解決不了緩衝池污染的問題。
當某一個SQL語句,要批量掃描大量數據時,可能致使把緩衝池的全部頁都替換出去,致使大量熱數據被換出,MySQL性能急劇降低,這種狀況叫緩衝池污染。
例如,有一個數據量較大的用戶表,當執行:
select * from user where name like "%shenjian%";
雖然結果集可能只有少許數據,但這類like不能命中索引,必須全表掃描,就須要訪問大量的頁:
(1)把頁加到緩衝池(插入老生代頭部);
(2)從頁裏讀出相關的row(插入新生代頭部);
(3)row裏的name字段和字符串shenjian進行比較,若是符合條件,加入到結果集中;
(4)…直到掃描完全部頁中的全部row…
如此一來,全部的數據頁都會被加載到新生代的頭部,但只會訪問一次,真正的熱數據被大量換出。
MySQL緩衝池加入了一個「老生代停留時間窗口」的機制:
(1)假設T=老生代停留時間窗口;
(2)插入老生代頭部的頁,即便馬上被訪問,並不會馬上放入新生代頭部;
(3)只有知足「被訪問」而且「在老生代停留時間」大於T,纔會被放入新生代頭部;
繼續舉例,假如批量數據掃描,有51,52,53,54,55等五個頁面將要依次被訪問。
若是沒有「老生代停留時間窗口」的策略,這些批量被訪問的頁面,會換出大量熱數據。
加入「老生代停留時間窗口」策略後,短期內被大量加載的頁,並不會馬上插入新生代頭部,而是優先淘汰那些,短時間內僅僅訪問了一次的頁。
而只有在老生代呆的時間足夠久,停留時間大於T,纔會被插入新生代頭部。
有三個比較重要的參數。
參數:innodb_buffer_pool_size
介紹:配置緩衝池的大小,在內存容許的狀況下,DBA每每會建議調大這個參數,越多數據和索引放到內存裏,數據庫的性能會越好。
參數:innodb_old_blocks_pct
介紹:老生代佔整個LRU鏈長度的比例,默認是37,即整個LRU中新生代與老生代長度比例是63:37。
畫外音:若是把這個參數設爲100,就退化爲普通LRU了。
參數:innodb_old_blocks_time
介紹:老生代停留時間窗口,單位是毫秒,默認是1000,即同時知足「被訪問」與「在老生代停留時間超過1秒」兩個條件,纔會被插入到新生代頭部。
(1)緩衝池(buffer pool)是一種常見的下降磁盤訪問的機制;
(2)緩衝池一般以頁(page)爲單位緩存數據;
(3)緩衝池的常見管理算法是LRU,memcache,OS,InnoDB都使用了這種算法;
(4)InnoDB對普通LRU進行了優化:
思路,比結論重要。
解決了什麼問題,比方案重要。
架構師之路-分享技術思路
相關推薦:
《寫一個cache,要掌握哪些技術點》
《6條shell小技巧,讓腳本更專業 | 1分鐘系列》
《MyISAM與InnoDB的索引差別 | 1分鐘系列》
《兩個小工具,分析MySQL死鎖》
調研:緩衝池是緩存的差異是啥?畫外音:長文閱讀和轉發低,爲啥?