緩衝池是數據庫最終的概念,數據庫能夠將一部分數據頁放在內存中造成緩衝池,當須要一個數據頁時,首先檢查內存中的緩衝池是否有這個頁面,若是有則直接命中返回,沒有則從磁盤中讀取這一頁,而後緩存到內存並返回。算法
可是內存的價值較高,通常來講服務器的內存老是小於磁盤大小的,並且內存不能徹底分配給數據庫做爲緩衝池。這就意味着數據庫基本上沒法將全部的數據都緩衝到內存中。數據庫
當緩衝池滿後,若是還有新的頁面要被緩衝到池中,就要設計一種頁面置換的算法,將一箇舊的頁面替換成新的頁面。緩存
通常來講咱們熟悉的算法有下面幾種:服務器
下面逐一介紹各類算法。性能
若是被替換掉的頁是之後不再會使用的,那麼這種算法無疑是最優秀的。由於無論什麼算法,替換掉的頁也有可能再次被緩存,替換掉其它的頁。測試
可是這種算法是沒法實現的,咱們不可能知道哪一個頁面之後也在不會被使用。設計
或者咱們退一步,將這個算法改爲被替換掉的頁是之後很長一段時間都不會再次被使用的,那麼這種算法無疑也是最優秀的。指針
可是仍是會面對一個沒法實現的問題,咱們仍是不知道哪些頁面會在將來多長一段時間內不會被再次訪問。頁面沒法確認,時間也沒法肯定。blog
雖然這種算法沒法被實現,可是能夠做爲一種度量,若是有一種算法其效率最接近OPT,那麼這種算法無疑是優秀的算法。隊列
先進先出算法是一種很簡單的算法,其基本思想是造成一個隊列,最早入隊的頁面最早被逐出。咱們用示意圖來模擬一下FIFO算法:
咱們的內存假設只能保存4個頁面,此時的訪問請求按照時間順序是1->2->3->4->5,那麼按照時間順序,當訪問到4號頁面時隊列正好填滿,當要訪問5號頁面時,會將最早入隊的1號頁面逐出。
這種算法實現起來很簡單,可是從實現上來看,性能和OPT算法差距最大。由於被替換出去的頁面頗有多是最常使用的頁面,所以這個算法不多見出如今數據庫緩衝池管理中的。
FIFO算法會出現一個叫作Belay異常的現象,就這個現象咱們解釋以下。
咱們首先定義一個4個頁面長度的隊列做爲緩衝池,而後按照下面的順序訪問:1->2->3->4->5->3->9->1->4->2->7->4->7。那麼咱們按照剛纔描述的FIFO來看看訪問的過程:
訪問順序 | 訪問頁 | 內存隊列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 2,3,4,5 | 否 |
6 | 3 | 2,3,4,5 | 是 |
7 | 9 | 3,4,5,9 | 否 |
8 | 1 | 4,5,9,1 | 否 |
9 | 4 | 4,5,9,1 | 是 |
10 | 2 | 5,9,1,2 | 否 |
11 | 7 | 9,1,2,7 | 是 |
12 | 4 | 1,2,7,4 | 否 |
13 | 7 | 1,2,7,4 | 是 |
從這個表格上看到,非命中次數有9次,那麼咱們將這個隊列的容量增長到5,而後再次重複這個訪問序列,看看效果:
訪問順序 | 訪問頁 | 內存隊列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 1,2,3,4,5 | 否 |
6 | 3 | 1,2,3,4,5 | 是 |
7 | 9 | 2,3,4,5,9 | 否 |
8 | 1 | 3,4,5,9,1 | 是 |
9 | 4 | 3,4,5,9,1 | 是 |
10 | 2 | 4,5,9,1,2 | 否 |
11 | 7 | 5,9,1,2,7 | 否 |
12 | 4 | 9,1,2,7,4 | 否 |
13 | 7 | 9,1,2,7,4 | 否 |
這樣的話,非命中的次數是10次,奇怪的是增長了緩衝池的容量,非命中緩衝的數量還增長了,這種現象就叫作Belay異常。
這種算法不該該被考慮。
LRU算法的思想也很簡單,實現一個鏈表(雙向鏈表),每次要緩衝新的頁面時,遍歷鏈表,選擇最近最少使用的頁面進行逐出操做。
這種算法要求每一個頁面上記錄一個上次使用時間t,程序決定逐出時,以這個時間t爲準,t距離當前時間最大的,就是要被逐出的頁面。
下圖中按照1->5->2->2->6->5->4的順序訪問,內存和訪問示意圖以下:
其中最接近頂端的頁面咱們認爲其t最小,最接近底部,咱們認爲其t最大。
訪問6號頁面的時候,內存被填滿,下一次訪問5號頁面的時候,會將5號頁面提高到頂部,也就是t最小,以後訪問4號頁面,由於原先內存中沒有4號頁面,所以會選擇逐出一個頁面。此時1號頁面在底部,其t最大,所以被逐出。
那麼LRU算法是否解決了Belay異常呢?
仍是按照上一節的實驗順序,測試容量爲4和5的內存,左側到右側,t逐漸增大:
訪問順序 | 訪問頁 | 內存隊列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 2,3,4,5 | 否 |
6 | 3 | 2,4,5,3 | 是 |
7 | 9 | 4,5,3,9 | 否 |
8 | 1 | 5,3,9,1 | 否 |
9 | 4 | 3,9,1,4 | 否 |
10 | 2 | 9,1,4,2 | 否 |
11 | 7 | 1,4,2,7 | 否 |
12 | 4 | 1,2,7,4 | 是 |
13 | 7 | 1,2,4,7 | 是 |
一共有10次未命中。增長容量到5,看一下新的狀況:
訪問順序 | 訪問頁 | 內存隊列 | 是否命中 |
---|---|---|---|
1 | 1 | 1 | 否 |
2 | 2 | 1,2 | 否 |
3 | 3 | 1,2,3 | 否 |
4 | 4 | 1,2,3,4 | 否 |
5 | 5 | 1,2,3,4,5 | 否 |
6 | 3 | 1,2,4,5,3 | 是 |
7 | 9 | 2,4,5,3,9 | 否 |
8 | 1 | 4,5,3,9,1 | 否 |
9 | 4 | 5,3,9,1,4 | 是 |
10 | 2 | 3,9,1,4,2 | 否 |
11 | 7 | 9,1,4,2,7 | 否 |
12 | 4 | 9,1,2,7,4 | 是 |
13 | 7 | 9,1,2,4,7 | 是 |
未命中的次數已經變成了9次,減小了一次,若是我設計的隊列中有大量的重複,那麼這個改進應該更加明顯。
LRU算法在InnoDB的實現中是被改進的,每次新添加進去的頁面會被放在隊列的3/8處。
不管如何,LRU算法都被認爲是最接近OPT的算法。
時鐘置換算法能夠認爲是一種最近未使用算法,即逐出的頁面都是最近沒有使用的那個。咱們給每個頁面設置一個標記位u,u=1表示最近有使用u=0則表示該頁面最近沒有被使用,應該被逐出。
按照1-2-3-4的順序訪問頁面,則緩衝池會以這樣的一種順序被填滿:
注意中間的指針,就像是時鐘的指針同樣在移動,這樣的訪問結束後,緩衝池裏如今已經被填滿了,此時若是要按照1-5的順序訪問,那麼在訪問1的時候是能夠直接命中緩存返回的,可是訪問5的時候,由於緩衝池已經滿了,因此要進行一次逐出操做,其操做示意圖以下:
最初要通過一輪遍歷,每次遍歷到一個節點發現u=1的,將該標記位置爲0,而後遍歷下一個頁面,一輪遍歷完後,發現沒有能夠被逐出的頁面,則進行下一輪遍歷,此次遍歷以後發現原先1號頁面的標記位u=0,則將該頁面逐出,置換爲頁面5,並將指針指向下一個頁面。
假設咱們接下來會訪問2號頁面,那麼能夠直接命中指針指向的頁面,並將這個頁面的標記爲u置爲1。
可是考慮一個問題,數據庫裏逐出的頁面是要寫回磁盤的,這是一個很昂貴的操做,所以咱們應該優先考慮逐出那些沒有被修改的頁面,這樣能夠下降IO。
所以在時鐘置換算法的基礎上能夠作一個改進,就是增長一個標記爲m,修改過標記爲1,沒有修改過則標記爲0。那麼u和m組成了一個元組,有四種可能,其被逐出的優先順序也不同: