咱們經常使用緩存提高數據查詢速度,因爲緩存容量有限,當緩存容量到達上限,就須要刪除部分數據挪出空間,這樣新數據才能夠添加進來。緩存數據不能隨機刪除,通常狀況下咱們須要根據某種算法刪除緩存數據。經常使用淘汰算法有 LRU,LFU,FIFO,這篇文章咱們聊聊 LRU 算法。java
LRU 是 Least Recently Used 的縮寫,這種算法認爲最近使用的數據是熱門數據,下一次很大機率將會再次被使用。而最近不多被使用的數據,很大機率下一次再也不用到。當緩存容量的滿時候,優先淘汰最近不多使用的數據。node
假設如今緩存內部數據如圖所示:算法
這裏咱們將列表第一個節點稱爲頭結點,最後一個節點爲尾結點。數據庫
當調用緩存獲取 key=1 的數據,LRU 算法須要將 1 這個節點移動到頭結點,其他節點不變,如圖所示。編程
而後咱們插入一個 key=8 節點,此時緩存容量到達上限,因此加入以前須要先刪除數據。因爲每次查詢都會將數據移動到頭結點,未被查詢的數據就將會下沉到尾部節點,尾部的數據就能夠認爲是最少被訪問的數據,因此刪除尾結點的數據。緩存
而後咱們直接將數據添加到頭結點。數據結構
這裏總結一下 LRU 算法具體步驟:ide
上面例子中能夠看到,LRU 算法須要添加頭節點,刪除尾結點。而鏈表添加節點/刪除節點時間複雜度 O(1),很是適合當作存儲緩存數據容器。可是不能使用普通的單向鏈表,單向鏈表有幾點劣勢:this
針對以上問題,能夠結合其餘數據結構解決。idea
使用散列表存儲節點,獲取節點的複雜度將會下降爲 O(1)。節點移動問題能夠在節點中再增長前驅指針,記錄上一個節點信息,這樣鏈表就從單向鏈表變成了雙向鏈表。
綜上使用雙向鏈表加散列表結合體,數據結構如圖所示:
在雙向鏈表中特地增長兩個『哨兵』節點,不用來存儲任何數據。使用哨兵節點,增長/刪除節點的時候就能夠不用考慮邊界節點不存在狀況,簡化編程難度,下降代碼複雜度。
LRU 算法實現代碼以下,爲了簡化 key ,val 都認爲 int 類型。
public class LRUCache {
Entry head, tail;
int capacity;
int size;
Map<Integer, Entry> cache;
public LRUCache(int capacity) {
this.capacity = capacity;
// 初始化鏈表
initLinkedList();
size = 0;
cache = new HashMap<>(capacity + 2);
}
/** * 若是節點不存在,返回 -1.若是存在,將節點移動到頭結點,並返回節點的數據。 * * @param key * @return */
public int get(int key) {
Entry node = cache.get(key);
if (node == null) {
return -1;
}
// 存在移動節點
moveToHead(node);
return node.value;
}
/** * 將節點加入到頭結點,若是容量已滿,將會刪除尾結點 * * @param key * @param value */
public void put(int key, int value) {
Entry node = cache.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
return;
}
// 不存在。先加進去,再移除尾結點
// 此時容量已滿 刪除尾結點
if (size == capacity) {
Entry lastNode = tail.pre;
deleteNode(lastNode);
cache.remove(lastNode.key);
size--;
}
// 加入頭結點
Entry newNode = new Entry();
newNode.key = key;
newNode.value = value;
addNode(newNode);
cache.put(key, newNode);
size++;
}
private void moveToHead(Entry node) {
// 首先刪除原來節點的關係
deleteNode(node);
addNode(node);
}
private void addNode(Entry node) {
head.next.pre = node;
node.next = head.next;
node.pre = head;
head.next = node;
}
private void deleteNode(Entry node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
public static class Entry {
public Entry pre;
public Entry next;
public int key;
public int value;
public Entry(int key, int value) {
this.key = key;
this.value = value;
}
public Entry() {
}
}
private void initLinkedList() {
head = new Entry();
tail = new Entry();
head.next = tail;
tail.pre = head;
}
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3);
System.out.println(cache.get(2));
}
}
複製代碼
緩存命中率是緩存系統的很是重要指標,若是緩存系統的緩存命中率太低,將會致使查詢迴流到數據庫,致使數據庫的壓力升高。
結合以上分析 LRU 算法優缺點。
LRU 算法優點在於算法實現難度不大,對於對於熱點數據, LRU 效率會很好。
LRU 算法劣勢在於對於偶發的批量操做,好比說批量查詢歷史數據,就有可能使緩存中熱門數據被這些歷史數據替換,形成緩存污染,致使緩存命中率降低,減慢了正常數據查詢。
如下方案來源與 MySQL InnoDB LRU 改進算法
將鏈表拆分紅兩部分,分爲熱數據區,與冷數據區,如圖所示。
改進以後算法流程將會變成下面同樣:
對於偶發的批量查詢,數據僅僅只會落入冷數據區,而後很快就會被淘汰出去。熱門數據區的數據將不會受到影響,這樣就解決了 LRU 算法緩存命中率降低的問題。
其餘改進方法還有 LRU-K,2Q,LIRS 算法,感興趣同窗能夠自行查閱。
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客:studyidea.cn