你與解決“緩存污染”只差這篇文章的距離

微信公衆號:IT一刻鐘
大型現實非嚴肅主義現場
一刻鐘與你分享優質技術架構與見聞,做一個有劇情的程序員
關注可第一時間瞭解更多精彩內容,定期有福利相送喲。

什麼是緩存污染?

由於緩存的讀取速度比非緩存要快上很多,所以在高性能場景下,系統在讀取數據時,是首先從緩存中查找需要的數據,如果找到了則直接讀取結果,如果找不到的話,則從內存或者硬盤中查找,再將查找到的結果存入緩存,以備下次使用。
實際上,對於一個系統來說,緩存的空間是有限且寶貴的,我們不可能將所有的數據都放入緩存中進行操作,即便可以數據安全性也得不到保證,而且,如果緩存的數據量過大大,其速度也會變得越來越慢。
這個時候就需要考慮緩存的淘汰機制,但是淘汰哪些數據,又保留哪些數據,這是一個問題。如果處理不得當,就會造成“緩存污染”問題。
而緩存污染,是指系統將不常用的數據從內存移到緩存,造成常用數據的擠出,降低了緩存效率的現象。

解決緩存污染的算法

LFU算法

LFU,英文名Least Frequently Used,字面意思就是最不經常使用的淘汰掉算法,是通過數據被訪問的頻率來判斷一個數據的熱點情況。其核心理念是“歷史上這個數據被訪問次數越多,那麼將來其被訪問的次數也多”。
LFU中每個數據塊都有一個引用計數器,所有數據塊按照引用數從大到小的排序。

步驟:

  1. 新數據插入到尾部,並將計數設置爲1;
  2. 當隊列中的數據被訪問後,引用計數+1,然後重新排序,保持引用次數從大到小排序;
  3. 當空間不足,需要淘汰數據時,將尾部引用計數最小的數據塊刪除。

分析:由於是根據頻數進行熱點判斷和淘汰,所以先天具備避免偶發性、週期性批量操作導致臨時非熱點數據大量涌入緩存,擠出熱點數據的問題。
雖然具備這種先天優勢,但依舊存在另一種緩存污染問題,即歷史熱點數據污染當前熱點數據,如果系統訪問模式發生了改變,新的熱點數據需要計數累加超過舊熱點數據,才能將舊熱點數據進行淘汰,造成熱點效應滯後的問題。
複雜度與代價:每次操作都需要進行計數和排序,並且需要維護每個數據塊計數情況,會佔用較高的內存與cpu。

一個小思考,根據LFU算法,如何以O(1)時間複雜度實現get和put操作緩存?


LFU-Aging算法

LFU-Aging是基於LFU的改進算法,目的是解決歷史熱點數據對當前熱點數據的污染問題。有些數據在開始時使用次數很多,但以後就不再使用,這類數據將會長時間留在緩存中,所以“除了訪問次數外,還要考慮訪問時間”,這也是LFU-Aging的核心理念。
雖然算法將時間納入了考量範圍,但LFU-Aging並不是直接記錄數據的訪問時間,而是增加了一個最大平均引用計數的閾值,然後通過當前平均引用計數來標識時間,換句話說,就是將當前緩存中的平均引用計數值當作當前的生命年代,當這個生命年代超過了預設的閾值,就會將當前所有計數值減半,形成指數衰變的生命年代。

分析:優點是當訪問模式發生改變的時候,生命年代的指數衰變會使LFU-Aging能夠更快的適用新的數據訪問模式,淘汰舊的熱點數據。
複雜度與代價:在LFU的基礎上又增加平均引用次數判斷和統計處理,對cpu的消耗更高,並且當平均引用次數超過指定閾值(Aging)後,還需要遍歷每一個數據塊的引用計數,進行指數衰變。


Window-LFU算法

Window-LFU顧名思義叫做窗口期LFU,區別於原義LFU中記錄所有數據的訪問歷史,Window-LFU只記錄過去一段時間內(窗口期)的訪問歷史,相當於給緩存設置了有效期限,過期數據不再緩存。當需要淘汰時,將這個窗口期內的數據按照LFU算法進行淘汰。
分析:由於是維護一段窗口期的記錄,數據量會比較少,所以內存佔用和cpu消耗都比LFU要低。並且這段窗口期相當於給緩存設置了有效期,能夠更快的適應新的訪問模式的變化,緩存污染問題基本不嚴重。
複雜度與代價:維護一段時期內的數據訪問記錄,並對其排序。


LRU算法

LRU算法,英文名Least Recently Used,意思是最近最少使用的淘汰算法,根據數據的歷史訪問記錄來進行淘汰數據,核心思想是“如果數據最近被訪問過1次,那麼將來被訪問的概率會更高”,類似於就近優先原則。

步驟:

  1. 新數據插入到鏈表頭部;
  2. 每當命中緩存,便將命中的緩存數據移到鏈表頭部;
  3. 當鏈表滿的時候,將鏈表尾部的數據丟棄。

分析:偶發性的、週期性的批量操作會使臨時數據涌入緩存,擠出熱點數據,導致LRU熱點命中率急劇下降,緩存污染情況比較嚴重。
複雜度與代價:數據結構複雜度較低;每次需要遍歷鏈表,找到命中的數據塊,然後將數據移到頭部。


LRU-K算法

LRU-K是基於LRU算法的優化版,其中K代表最近訪問的次數,從某種意義上,LRU可以看作是LRU-1算法,引入K的意義是爲了解決上面所提到的緩存污染問題。其核心理念是從“數據最近被訪問過1次”蛻變成“數據最近被訪問過K次,那麼將來被訪問的概率會更高”。
LRU-K與LRU區別是,LRU-K多了一個數據訪問歷史記錄隊列(需要注意的是,訪問歷史記錄隊列並不是緩存隊列,所以是不保存數據本身的,只是保存對數據的訪問記錄,數據此時依舊在原始存儲中),隊列中維護着數據被訪問的次數以及時間戳,只有當這個數據被訪問的次數大於等於K值時,纔會從歷史記錄隊列中刪除,然後把數據加入到緩存隊列中去。

步驟:

  1. 數據第一次被訪問時,加入到歷史訪問記錄隊列中,訪問次數爲1,初始化訪問時間戳;
  2. 如果數據訪問次數沒有達到K次,則訪問次數+1,更新時間戳。當隊列滿了時,按照某種規則(LRU或者FIFO)將歷史記錄淘汰。爲了避免歷史數據污染未來數據的問題,還需要加上一個有效期限,對超過有效期的訪問記錄,進行重新計數。(可以使用懶處理,即每次對訪問記錄做處理時,先將記錄中的訪問時間與當前時間進行對比,如果時間間隔超過預設的值,則訪問次數重置爲1並更新時間戳,表示重新開始計數)
  3. 當數據訪問計數大於等於K次後,將數據從歷史訪問隊列中刪除,更新數據時間戳,保存到緩存隊列頭部中(緩存隊列時間戳遞減排序,越到尾部距離當前時間越長);
  4. 緩存隊列中數據被再次訪問後,將其移到頭部,並更新時間戳;
  5. 緩存隊列需要淘汰數據時,淘汰緩存隊列中排在末尾的數據,即:淘汰“倒數第K次訪問離現在最久”的數據。

分析:LRU-K降低了“緩存污染”帶來的問題,命中率比LRU要高。實際應用中LRU-2是綜合各種因素後最優的選擇,LRU-3或者更大的K值命中率會高,但適應性差,一旦訪問模式發生變化,需要大量的新數據訪問才能將歷史熱點訪問記錄清除掉。
複雜度與代價:LRU-K隊列是一個優先級隊列。由於LRU-K需要記錄那些被訪問過,但還沒有放入緩存的對象,導致內存消耗會很多。


URL-Two queues算法

URL-Two queues算法類似於LRU-2,不同點在於URL-Two queues將LRU-2算法中的訪問歷史隊列(注意這不是緩存數據的)改爲一個FIFO緩存隊列,即:URL-Two queues算法有兩個緩存隊列,一個是FIFO隊列(First in First out,先進先出),一個是LRU隊列。
當數據第一次訪問時,URL-Two queues算法將數據緩存在FIFO隊列裏面,當數據第二次被訪問時,則將數據從FIFO隊列移到LRU隊列裏面,兩個隊列各自按照自己的方法淘汰數據。

步驟:

  1. 新訪問的數據先插入到FIFO隊列中;
  2. 如果數據在FIFO隊列中一直沒有被再次訪問,則最終按照FIFO規則淘汰;
  3. 如果數據在FIFO隊列中被再次訪問,則將數據從FIFO刪除,加入到LRU隊列頭部;
  4. 如果數據在LRU隊列再次被訪問,則將數據移到LRU隊列頭部;
  5. LRU隊列淘汰末尾的數據。

分析:URL-Two queues算法和LRU-2算法命中率類似,但是URL-Two queues會減少一次從原始存儲讀取或計算數據的操作。命中率要高於LRU。
複雜度與代價:需要維護兩個隊列,代價是FIFO和LRU代價之和。


五三LRU算法

emmmm...
這個名字其實是我取的,大概是這種算法還沒有被命名?當然,這是一個玩笑話。
我是在mysql底層實現裏發現這個算法的,mysql在處理緩存淘汰時是用的這個方法,有點像URL-Two queues的變體,只是我們只需要維護一個隊列,然後將隊列按照5:3的比例進行分割,5的那部分叫做young區,3的那部分叫做old區。具體是怎麼樣的請先看我把圖畫出來:

步驟:

  1. 第一次訪問的數據從隊列的3/8處位置插入;
  2. 如果數據再次被訪問,則移動到隊列頭部;
  3. 如果數據沒有被再訪問,會逐步被熱點數據驅逐向下移;
  4. 淘汰尾部數據。

分析:五三LRU算法算作是URL-Two queues算法的變種,原理其實是一樣的,只是把兩個隊列合二爲一個隊列進行數據的處理,所以命中率和URL-Two queues算法一樣。
複雜度與代價:維護一個隊列,代價較低,但是內存佔用率和URL-Two queues一樣。


Multi Queue算法

Multi Queue算法根據訪問頻率將數據劃分爲多個隊列,不同的隊列具有不同的訪問優先級,其核心思想是“優先緩存訪問次數多的數據”。
Multi Queue算法將緩存劃分爲多個LRU隊列,每個隊列對應不同的訪問優先級。訪問優先級是根據訪問次數計算出來的,例如:
Q0,Q1....Qn代表不同的優先級隊列,Q-history代表從緩存中淘汰數據,但記錄了數據的索引和引用次數。

步驟:

  1. 新插入的數據放入Q0;
  2. 每個隊列按照LRU管理數據,再次訪問的數據移動到頭部;
  3. 當數據的訪問次數達到一定次數,需要提升優先級時,將數據從當前隊列刪除,加入到高一級隊列的頭部;
  4. 爲了防止高優先級數據永遠不被淘汰,當數據在指定的時間裏訪問沒有被訪問時,需要降低優先級,將數據從當前隊列刪除,加入到低一級的隊列頭部;
  5. 需要淘汰數據時,從最低一級隊列開始按照LRU淘汰;每個隊列淘汰數據時,將數據從緩存中刪除,將數據索引加入Q-history頭部;
  6. 如果數據在Q-history中被重新訪問,則重新計算其優先級,移到目標隊列的頭部;
  7. Q-history按照LRU淘汰數據的索引。

分析:Multi Queue降低了“緩存污染”帶來的問題,命中率比LRU要高。
複雜度與代價:Multi Queue需要維護多個隊列,且需要維護每個數據的訪問時間,複雜度比LRU高。Multi Queue需要記錄每個數據的訪問時間,需要定時掃描所有隊列,代價比LRU要高。雖然Multi Queue的隊列看起來數量比較多,但由於所有隊列之和受限於緩存容量的大小,因此這裏多個隊列長度之和和一個LRU隊列是一樣的,因此隊列掃描性能也相近。

說在後面話

還有哪些優秀的緩存淘汰算法,或者你有更好的想法或問題,歡迎留言給我!

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章