久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

    1. 站長資訊網
      最全最豐富的資訊網站

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      本篇文章給大家?guī)砹薽ysql中關于Buffer pool的相關知識,其中包括了數據頁、緩存頁free鏈表、 flush鏈表、 LRU鏈表Chunk等等,希望對大家有幫助。

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      緩存的重要性

      通過前邊的嘮叨我們知道,對于使用InnoDB作為存儲引擎的表來說,不管是用于存儲用戶數據的索引(包括聚簇索引和二級索引),還是各種系統數據,都是以的形式存放在表空間中的,而所謂的表空間只不過是InnoDB對文件系統上一個或幾個實際文件的抽象,也就是說我們的數據說到底還是存儲在磁盤上的。但是各位也都知道,磁盤的速度慢的跟烏龜一樣,怎么能配得上“快如風,疾如電”的CPU呢?所以InnoDB存儲引擎在處理客戶端的請求時,當需要訪問某個頁的數據時,就會把完整的頁的數據全部加載到內存中,也就是說即使我們只需要訪問一個頁的一條記錄,那也需要先把整個頁的數據加載到內存中。將整個頁加載到內存中后就可以進行讀寫訪問了,在進行完讀寫訪問之后并不著急把該頁對應的內存空間釋放掉,而是將其緩存起來,這樣將來有請求再次訪問該頁面時,就可以省去磁盤IO的開銷了。

      InnoDB的Buffer Pool

      啥是個Buffer Pool

      設計InnoDB的大叔為了緩存磁盤中的頁,在MySQL服務器啟動的時候就向操作系統申請了一片連續(xù)的內存,他們給這片內存起了個名,叫做Buffer Pool(中文名是緩沖池)。那它有多大呢?這個其實看我們機器的配置,如果你是土豪,你有512G內存,你分配個幾百G作為Buffer Pool也可以啊,當然你要是沒那么有錢,設置小點也行呀~ 默認情況下Buffer Pool只有128M大小。當然如果你嫌棄這個128M太大或者太小,可以在啟動服務器的時候配置innodb_buffer_pool_size參數的值,它表示Buffer Pool的大小,就像這樣:

      [server] innodb_buffer_pool_size = 268435456

      其中,268435456的單位是字節(jié),也就是我指定Buffer Pool的大小為256M。需要注意的是,Buffer Pool也不能太小,最小值為5M(當小于該值時會自動設置成5M)。

      Buffer Pool內部組成

      Buffer Pool中默認的緩存頁大小和在磁盤上默認的頁大小是一樣的,都是16KB。為了更好的管理這些在Buffer Pool中的緩存頁,設計InnoDB的大叔為每一個緩存頁都創(chuàng)建了一些所謂的控制信息,這些控制信息包括該頁所屬的表空間編號、頁號、緩存頁在Buffer Pool中的地址、鏈表節(jié)點信息、一些鎖信息以及LSN信息(鎖和LSN我們之后會具體嘮叨,現在可以先忽略),當然還有一些別的控制信息,我們這就不全嘮叨一遍了,挑重要的說嘛~

      每個緩存頁對應的控制信息占用的內存大小是相同的,我們就把每個頁對應的控制信息占用的一塊內存稱為一個控制塊吧,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 后邊,所以整個Buffer Pool對應的內存空間看起來就是這樣的:

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      咦?控制塊和緩存頁之間的那個碎片是個什么玩意兒?你想想啊,每一個控制塊都對應一個緩存頁,那在分配足夠多的控制塊和緩存頁后,可能剩余的那點兒空間不夠一對控制塊和緩存頁的大小,自然就用不到嘍,這個用不到的那點兒內存空間就被稱為碎片了。當然,如果你把Buffer Pool的大小設置的剛剛好的話,也可能不會產生碎片

      小貼士: 每個控制塊大約占用緩存頁大小的5%,在MySQL5.7.21這個版本中,每個控制塊占用的大小是808字節(jié)。而我們設置的innodb_buffer_pool_size并不包含這部分控制塊占用的內存空間大小,也就是說InnoDB在為Buffer Pool向操作系統申請連續(xù)的內存空間時,這片連續(xù)的內存空間一般會比innodb_buffer_pool_size的值大5%左右。

      free鏈表的管理

      當我們最初啟動MySQL服務器的時候,需要完成對Buffer Pool的初始化過程,就是先向操作系統申請Buffer Pool的內存空間,然后把它劃分成若干對控制塊和緩存頁。但是此時并沒有真實的磁盤頁被緩存到Buffer Pool中(因為還沒有用到),之后隨著程序的運行,會不斷的有磁盤上的頁被緩存到Buffer Pool中。那么問題來了,從磁盤上讀取一個頁到Buffer Pool中的時候該放到哪個緩存頁的位置呢?或者說怎么區(qū)分Buffer Pool中哪些緩存頁是空閑的,哪些已經被使用了呢?我們最好在某個地方記錄一下Buffer Pool中哪些緩存頁是可用的,這個時候緩存頁對應的控制塊就派上大用場了,我們可以把所有空閑的緩存頁對應的控制塊作為一個節(jié)點放到一個鏈表中,這個鏈表也可以被稱作free鏈表(或者說空閑鏈表)。剛剛完成初始化的Buffer Pool中所有的緩存頁都是空閑的,所以每一個緩存頁對應的控制塊都會被加入到free鏈表中,假設該Buffer Pool中可容納的緩存頁數量為n,那增加了free鏈表的效果圖就是這樣的:

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      從圖中可以看出,我們?yōu)榱斯芾砗眠@個free鏈表,特意為這個鏈表定義了一個基節(jié)點,里邊兒包含著鏈表的頭節(jié)點地址,尾節(jié)點地址,以及當前鏈表中節(jié)點的數量等信息。這里需要注意的是,鏈表的基節(jié)點占用的內存空間并不包含在為Buffer Pool申請的一大片連續(xù)內存空間之內,而是單獨申請的一塊內存空間。

      小貼士: 鏈表基節(jié)點占用的內存空間并不大,在MySQL5.7.21這個版本里,每個基節(jié)點只占用40字節(jié)大小。后邊我們即將介紹許多不同的鏈表,它們的基節(jié)點和free鏈表的基節(jié)點的內存分配方式是一樣一樣的,都是單獨申請的一塊40字節(jié)大小的內存空間,并不包含在為Buffer Pool申請的一大片連續(xù)內存空間之內。

      有了這個free鏈表之后事兒就好辦了,每當需要從磁盤中加載一個頁到Buffer Pool中時,就從free鏈表中取一個空閑的緩存頁,并且把該緩存頁對應的控制塊的信息填上(就是該頁所在的表空間、頁號之類的信息),然后把該緩存頁對應的free鏈表節(jié)點從鏈表中移除,表示該緩存頁已經被使用了~

      緩存頁的哈希處理

      我們前邊說過,當我們需要訪問某個頁中的數據時,就會把該頁從磁盤加載到Buffer Pool中,如果該頁已經在Buffer Pool中的話直接使用就可以了。那么問題也就來了,我們怎么知道該頁在不在Buffer Pool中呢?難不成需要依次遍歷Buffer Pool中各個緩存頁么?一個Buffer Pool中的緩存頁這么多都遍歷完豈不是要累死?

      再回頭想想,我們其實是根據表空間號 + 頁號來定位一個頁的,也就相當于表空間號 + 頁號是一個key緩存頁就是對應的value,怎么通過一個key來快速找著一個value呢?哈哈,那肯定是哈希表嘍~

      小貼士: 啥?你別告訴我你不知道哈希表是個啥?我們這個文章不是講哈希表的,如果你不會那就去找本數據結構的書看看吧~ 啥?外頭的書看不懂?別急,等我~

      所以我們可以用表空間號 + 頁號作為key,緩存頁作為value創(chuàng)建一個哈希表,在需要訪問某個頁的數據時,先從哈希表中根據表空間號 + 頁號看看有沒有對應的緩存頁,如果有,直接使用該緩存頁就好,如果沒有,那就從free鏈表中選一個空閑的緩存頁,然后把磁盤中對應的頁加載到該緩存頁的位置。

      flush鏈表的管理

      如果我們修改了Buffer Pool中某個緩存頁的數據,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱為臟頁(英文名:dirty page)。當然,最簡單的做法就是每發(fā)生一次修改就立即同步到磁盤上對應的頁上,但是頻繁的往磁盤中寫數據會嚴重的影響程序的性能(畢竟磁盤慢的像烏龜一樣)。所以每次修改緩存頁后,我們并不著急立即把修改同步到磁盤上,而是在未來的某個時間點進行同步,至于這個同步的時間點我們后邊會作說明說明的,現在先不用管哈~

      但是如果不立即同步到磁盤的話,那之后再同步的時候我們怎么知道Buffer Pool中哪些頁是臟頁,哪些頁從來沒被修改過呢?總不能把所有的緩存頁都同步到磁盤上吧,假如Buffer Pool被設置的很大,比方說300G,那一次性同步這么多數據豈不是要慢死!所以,我們不得不再創(chuàng)建一個存儲臟頁的鏈表,凡是修改過的緩存頁對應的控制塊都會作為一個節(jié)點加入到一個鏈表中,因為這個鏈表節(jié)點對應的緩存頁都是需要被刷新到磁盤上的,所以也叫flush鏈表。鏈表的構造和free鏈表差不多,假設某個時間點Buffer Pool中的臟頁數量為n,那么對應的flush鏈表就長這樣:

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      LRU鏈表的管理

      緩存不夠的窘境

      Buffer Pool對應的內存大小畢竟是有限的,如果需要緩存的頁占用的內存大小超過了Buffer Pool大小,也就是free鏈表中已經沒有多余的空閑緩存頁的時候豈不是很尷尬,發(fā)生了這樣的事兒該咋辦?當然是把某些舊的緩存頁從Buffer Pool中移除,然后再把新的頁放進來嘍~ 那么問題來了,移除哪些緩存頁呢?

      為了回答這個問題,我們還需要回到我們設立Buffer Pool的初衷,我們就是想減少和磁盤的IO交互,最好每次在訪問某個頁的時候它都已經被緩存到Buffer Pool中了。假設我們一共訪問了n次頁,那么被訪問的頁已經在緩存中的次數除以n就是所謂的緩存命中率,我們的期望就是讓緩存命中率越高越好~ 從這個角度出發(fā),回想一下我們的微信聊天列表,排在前邊的都是最近很頻繁使用的,排在后邊的自然就是最近很少使用的,假如列表能容納下的聯系人有限,你是會把最近很頻繁使用的留下還是最近很少使用的留下呢?廢話,當然是留下最近很頻繁使用的了~

      簡單的LRU鏈表

      管理Buffer Pool的緩存頁其實也是這個道理,當Buffer Pool中不再有空閑的緩存頁時,就需要淘汰掉部分最近很少使用的緩存頁。不過,我們怎么知道哪些緩存頁最近頻繁使用,哪些最近很少使用呢?呵呵,神奇的鏈表再一次派上了用場,我們可以再創(chuàng)建一個鏈表,由于這個鏈表是為了按照最近最少使用的原則去淘汰緩存頁的,所以這個鏈表可以被稱為LRU鏈表(LRU的英文全稱:Least Recently Used)。當我們需要訪問某個頁時,可以這樣處理LRU鏈表

      • 如果該頁不在Buffer Pool中,在把該頁從磁盤加載到Buffer Pool中的緩存頁時,就把該緩存頁對應的控制塊作為節(jié)點塞到鏈表的頭部。

      • 如果該頁已經緩存在Buffer Pool中,則直接把該頁對應的控制塊移動到LRU鏈表的頭部。

      也就是說:只要我們使用到某個緩存頁,就把該緩存頁調整到LRU鏈表的頭部,這樣LRU鏈表尾部就是最近最少使用的緩存頁嘍~ 所以當Buffer Pool中的空閑緩存頁使用完時,到LRU鏈表的尾部找些緩存頁淘汰就OK啦,真簡單,嘖嘖…

      劃分區(qū)域的LRU鏈表

      高興的太早了,上邊的這個簡單的LRU鏈表用了沒多長時間就發(fā)現問題了,因為存在這兩種比較尷尬的情況:

      • 情況一:InnoDB提供了一個看起來比較貼心的服務——預讀(英文名:read ahead)。所謂預讀,就是InnoDB認為執(zhí)行當前的請求可能之后會讀取某些頁面,就預先把它們加載到Buffer Pool中。根據觸發(fā)方式的不同,預讀又可以細分為下邊兩種:

        • 線性預讀

          設計InnoDB的大叔提供了一個系統變量innodb_read_ahead_threshold,如果順序訪問了某個區(qū)(extent)的頁面超過這個系統變量的值,就會觸發(fā)一次異步讀取下一個區(qū)中全部的頁面到Buffer Pool的請求,注意異步讀取意味著從磁盤中加載這些被預讀的頁面并不會影響到當前工作線程的正常執(zhí)行。這個innodb_read_ahead_threshold系統變量的值默認是56,我們可以在服務器啟動時通過啟動參數或者服務器運行過程中直接調整該系統變量的值,不過它是一個全局變量,注意使用SET GLOBAL命令來修改哦。

          小貼士: InnoDB是怎么實現異步讀取的呢?在Windows或者Linux平臺上,可能是直接調用操作系統內核提供的AIO接口,在其它類Unix操作系統中,使用了一種模擬AIO接口的方式來實現異步讀取,其實就是讓別的線程去讀取需要預讀的頁面。如果你讀不懂上邊這段話,那也就沒必要懂了,和我們主題其實沒太多關系,你只需要知道異步讀取并不會影響到當前工作線程的正常執(zhí)行就好了。其實這個過程涉及到操作系統如何處理IO以及多線程的問題,找本操作系統的書看看吧,什么?操作系統的書寫的都很難懂?沒關系,等我~

        • 隨機預讀

          如果Buffer Pool中已經緩存了某個區(qū)的13個連續(xù)的頁面,不論這些頁面是不是順序讀取的,都會觸發(fā)一次異步讀取本區(qū)中所有其的頁面到Buffer Pool的請求。設計InnoDB的大叔同時提供了innodb_random_read_ahead系統變量,它的默認值為OFF,也就意味著InnoDB并不會默認開啟隨機預讀的功能,如果我們想開啟該功能,可以通過修改啟動參數或者直接使用SET GLOBAL命令把該變量的值設置為ON。

        預讀本來是個好事兒,如果預讀到Buffer Pool中的頁成功的被使用到,那就可以極大的提高語句執(zhí)行的效率??墒侨绻貌坏侥??這些預讀的頁都會放到LRU鏈表的頭部,但是如果此時Buffer Pool的容量不太大而且很多預讀的頁面都沒有用到的話,這就會導致處在LRU鏈表尾部的一些緩存頁會很快的被淘汰掉,也就是所謂的劣幣驅逐良幣,會大大降低緩存命中率。

      • 情況二:有的小伙伴可能會寫一些需要掃描全表的查詢語句(比如沒有建立合適的索引或者壓根兒沒有WHERE子句的查詢)。

        掃描全表意味著什么?意味著將訪問到該表所在的所有頁!假設這個表中記錄非常多的話,那該表會占用特別多的,當需要訪問這些頁時,會把它們統統都加載到Buffer Pool中,這也就意味著吧唧一下,Buffer Pool中的所有頁都被換了一次血,其他查詢語句在執(zhí)行時又得執(zhí)行一次從磁盤加載到Buffer Pool的操作。而這種全表掃描的語句執(zhí)行的頻率也不高,每次執(zhí)行都要把Buffer Pool中的緩存頁換一次血,這嚴重的影響到其他查詢對 Buffer Pool的使用,從而大大降低了緩存命中率。

      總結一下上邊說的可能降低Buffer Pool的兩種情況:

      • 加載到Buffer Pool中的頁不一定被用到。

      • 如果非常多的使用頻率偏低的頁被同時加載到Buffer Pool時,可能會把那些使用頻率非常高的頁從Buffer Pool中淘汰掉。

      因為有這兩種情況的存在,所以設計InnoDB的大叔把這個LRU鏈表按照一定比例分成兩截,分別是:

      • 一部分存儲使用頻率非常高的緩存頁,所以這一部分鏈表也叫做熱數據,或者稱young區(qū)域

      • 另一部分存儲使用頻率不是很高的緩存頁,所以這一部分鏈表也叫做冷數據,或者稱old區(qū)域。

      為了方便大家理解,我們把示意圖做了簡化,各位領會精神就好:

      深入了解MySQL原理篇之Buffer pool(圖文詳解)

      大家要特別注意一個事兒:我們是按照某個比例將LRU鏈表分成兩半的,不是某些節(jié)點固定是young區(qū)域的,某些節(jié)點固定是old區(qū)域的,隨著程序的運行,某個節(jié)點所屬的區(qū)域也可能發(fā)生變化。那這個劃分成兩截的比例怎么確定呢?對于InnoDB存儲引擎來說,我們可以通過查看系統變量innodb_old_blocks_pct的值來確定old區(qū)域在LRU鏈表中所占的比例,比方說這樣:

      mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct'; +-----------------------+-------+ | Variable_name         | Value | +-----------------------+-------+ | innodb_old_blocks_pct | 37    | +-----------------------+-------+ 1 row in set (0.01 sec)

      從結果可以看出來,默認情況下,old區(qū)域在LRU鏈表中所占的比例是37%,也就是說old區(qū)域大約占LRU鏈表3/8。這個比例我們是可以設置的,我們可以在啟動時修改innodb_old_blocks_pct參數來控制old區(qū)域在LRU鏈表中所占的比例,比方說這樣修改配置文件:

      [server] innodb_old_blocks_pct = 40

      這樣我們在啟動服務器后,old區(qū)域占LRU鏈表的比例就是40%。當然,如果在服務器運行期間,我們也可以修改這個系統變量的值,不過需要注意的是,這個系統變量屬于全局變量,一經修改,會對所有客戶端生效,所以我們只能這樣修改:

      SET GLOBAL innodb_old_blocks_pct = 40;

      有了這個被劃分成youngold區(qū)域的LRU鏈表之后,設計InnoDB的大叔就可以針對我們上邊提到的兩種可能降低緩存命中率的情況進行優(yōu)化了:

      • 針對預讀的頁面可能不進行后續(xù)訪問情況的優(yōu)化

        設計InnoDB的大叔規(guī)定,當磁盤上的某個頁面在初次加載到Buffer Pool中的某個緩存頁時,該緩存頁對應的控制塊會被放到old區(qū)域的頭部。這樣針對預讀到Buffer Pool卻不進行后續(xù)訪問的頁面就會被逐漸從old區(qū)域逐出,而不會影響young區(qū)域中被使用比較頻繁的緩存頁。

      • 針對全表掃描時,短時間內訪問大量使用頻率非常低的頁面情況的優(yōu)化

        在進行全表掃描時,雖然首次被加載到Buffer Pool的頁被放到了old區(qū)域的頭部,但是后續(xù)會被馬上訪問到,每次進行訪問的時候又會把該頁放到young區(qū)域的頭部,這樣仍然會把那些使用頻率比較高的頁面給頂下去。有同學會想:可不可以在第一次訪問該頁面時不將其從old區(qū)域移動到young區(qū)域的頭部,后續(xù)訪問時再將其移動到young區(qū)域的頭部?;卮鹗牵盒胁煌?!因為設計InnoDB的大叔規(guī)定每次去頁面中讀取一條記錄時,都算是訪問一次頁面,而一個頁面中可能會包含很多條記錄,也就是說讀取完某個頁面的記錄就相當于訪問了這個頁面好多次。

        咋辦?全表掃描有一個特點,那就是它的執(zhí)行頻率非常低,誰也不會沒事兒老在那寫全表掃描的語句玩,而且在執(zhí)行全表掃描的過程中,即使某個頁面中有很多條記錄,也就是去多次訪問這個頁面所花費的時間也是非常少的。所以我們只需要規(guī)定,在對某個處在old區(qū)域的緩存頁進行第一次訪問時就在它對應的控制塊中記錄下來這個訪問時間,如果后續(xù)的訪問時間與第一次訪問的時間在某個時間間隔內,那么該頁面就不會被從old區(qū)域移動到young區(qū)域的頭部,否則將它移動到young區(qū)域的頭部。上述的這個間隔時間是由系統變量innodb_old_blocks_time控制的,你看:

      mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time'; +------------------------+-------+ | Variable_name          | Value | +------------------------+-------+ | innodb_old_blocks_time | 1000  | +------------------------+-------+ 1 row in set (0.01 sec)

      這個innodb_old_blocks_time的默認值是1000,它的單位是毫秒,也就意味著對于從磁盤上被加載到LRU鏈表的old區(qū)域的某個頁來說,如果第一次和最后一次訪問該頁面的時間間隔小于1s(很明顯在一次全表掃描的過程中,多次訪問一個頁面中的時間不會超過1s),那么該頁是不會被加入到young區(qū)域的~ 當然,像innodb_old_blocks_pct一樣,我們也可以在服務器啟動或運行時設置innodb_old_blocks_time的值,這里就不贅述了,你自己試試吧~ 這里需要注意的是,如果我們把innodb_old_blocks_time的值設置為0,那么每次我們訪問一個頁面時就會把該頁面放到young區(qū)域的頭部。

      綜上所述,正是因為將LRU鏈表劃分為youngold區(qū)域這兩個部分,又添加了innodb_old_blocks_time這個系統變量,才使得預讀機制和全表掃描造成的緩存命中率降低的問題得到了遏制,因為用不到的預讀頁面以及全表掃描的頁面都只會被放到old區(qū)域,而不影響young區(qū)域中的緩存頁。

      更進一步優(yōu)化LRU鏈表

      LRU鏈表這就說完了么?沒有,早著呢~ 對于young區(qū)域的緩存頁來說,我們每次訪問一個緩存頁就要把它移動到LRU鏈表的頭部,這樣開銷是不是太大啦,畢竟在young區(qū)域的緩存頁都是熱點數據,也就是可能被經常訪問的,這樣頻繁的對LRU鏈表進行節(jié)點移動操作是不是不太好?。渴堑?,為了解決這個問題其實我們還可以提出一些優(yōu)化策略,比如只有被訪問的緩存頁位于young區(qū)域的1/4的后邊,才會被移動到LRU鏈表頭部,這樣就可以降低調整LRU鏈表的頻率,從而提升性能(也就是說如果某個緩存頁對應的節(jié)點在young區(qū)域的1/4中,再次訪問該緩存頁時也不會將其移動到LRU鏈表頭部)。

      小貼士: 我們之前介紹隨機預讀的時候曾說,如果Buffer Pool中有某個區(qū)的13個連續(xù)頁面就會觸發(fā)隨機預讀,這其實是不嚴謹的(不幸的是MySQL文檔就是這么說的[攤手]),其實還要求這13個頁面是非常熱的頁面,所謂的非常熱,指的是這些頁面在整個young區(qū)域的頭1/4處。

      還有沒有什么別的針對LRU鏈表的優(yōu)化措施呢?當然有啊,你要是好好學,寫篇論文,寫本書都不是問題,可是這畢竟是一個介紹MySQL基礎知識的文章,再說多了篇幅就受不了了,也影響大家的閱讀體驗,所以適可而止,想了解

      贊(0)
      分享到: 更多 (0)
      網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號