如何設計一個LRU Cache?
Google和百度的面試題都出現了設計一個Cache的題目,什麼是Cache,如何設計簡單的Cache,通過搜集資料,本文給出個總結。
通常的問題描述可以是這樣:
Question:
[1] Design a layer in front of a system which cache the last n requests and the responses to them from the system.
在一個系統之上設計一個Cache,緩衝最近的n個請求以及系統的響應。
what data structure would you use to implement the cache in the later to support following operations.
用什麼樣的資料結構設計這個Cache才能滿足下面的操作呢?
[a] When a request comes look it up in the cache and if it hits then return the response from here and do not pass the request to the system
[b] If the request is not found in the cache then pass it on to the system
[c] Since cache can only store the last n requests, Insert the n+1th request in the cache and delete one of the older requests from the cache
因為Cache只緩衝最新的n個請求,向Cache插入第n+1個請求時,從Cache中刪除最舊的請求。
[d]Design one cache such that all operations can be done in O(1) – lookup, delete and insert.
Cache簡介:
Cache(快取), 一個在電腦中幾乎隨時接觸的概念。CPU中Cache能極大提高存取資料和指令的時間,讓整個儲存空間(Cache+記憶體)既有Cache的高速度,又能有記憶體的大容量;作業系統中的記憶體page中使用的Cache能使得頻繁讀取的記憶體磁碟檔案較少的被置換出記憶體,從而提高訪問速度;資料庫中資料查詢也用到Cache來提高效率;即便是Powerbuilder的DataWindow資料處理也用到了Cache的類似設計。Cache的演算法設計常見的有FIFO(first in first out)和LRU(least
recently used)。根據題目的要求,顯然是要設計一個LRU的Cache。
解題思路:
Cache中的儲存空間往往是有限的,當Cache中的儲存塊被用完,而需要把新的資料Load進Cache的時候,我們就需要設計一種良好的演算法來完成資料區塊的替換。LRU的思想是基於“最近用到的資料被重用的機率比較早用到的大的多”這個設計規則來實現的。
為了能夠快速刪除最久沒有訪問的資料項目和插入最新的資料項目,我們雙向鏈表串連Cache中的資料項目,並且保證鏈表鑑效組資料項從最近訪問到最舊訪問的順序。每次資料項目被查詢到時,都將此資料項目移動到鏈表頭部(O(1)的時間複雜度)。這樣,在進行過多次尋找操作後,最近被使用過的內容就向鏈表的頭移動,而沒有被使用的內容就向鏈表的後面移動。當需要替換時,鏈表最後的位置就是最近最少被使用的資料項目,我們只需要將最新的資料項目放在鏈表頭部,當Cache滿時,淘汰鏈表最後的位置就是了。
註: 對於雙向鏈表的使用,基於兩個考慮。首先是Cache中塊的命中可能是隨機的,和Load進來的順序無關。其次,雙向鏈表插入、刪除很快,可以靈活的調整相互間的次序,時間複雜度為O(1)。
尋找一個鏈表中元素的時間複雜度是O(n),每次命中的時候,我們就需要花費O(n)的時間來進行尋找,如果不添加其他的資料結構,這個就是我們能實現的最高效率了。目前看來,整個演算法的瓶頸就是在尋找這裡了,怎麼樣才能提高尋找的效率呢?Hash表,對,就是它,資料結構中之所以有它,就是因為它的尋找時間複雜度是O(1)。
梳理一下思路:對於Cache的每個資料區塊,我們設計一個資料結構來儲存Cache塊的內容,並實現一個雙向鏈表,其中屬性next和prev時雙向鏈表的兩個指標,key用於儲存物件的索引值,value使用者儲存要cache塊對象本身。
Cache的介面:
查詢:
- 根據索引值查詢hashmap,若命中,則返回節點,否則返回null。
- 從雙向鏈表中刪除命中的節點,將其重新插入到表頭。
- 所有操作的複雜度均為O(1)。
插入:
- 將新的節點關聯到Hashmap
- 如果Cache滿了,刪除雙向鏈表的尾節點,同時刪除Hashmap對應的記錄
- 將新的節點插入到雙向鏈表中頭部
更新:
刪除:
LRU Cache的Java 實現:
public interface Cache<K extends Comparable, V> { V get(K obj); //查詢 void put(K key, V obj); //插入和更新 void put(K key, V obj, long validTime); void remove(K key); //刪除 Pair[] getAll(); int size();} public class Pair<K extends Comparable, V> implements Comparable<Pair> { public Pair(K key1, V value1) { this.key = key1; this.value = value1; } public K key; public V value; public boolean equals(Object obj) { if(obj instanceof Pair) { Pair p = (Pair)obj; return key.equals(p.key)&&value.equals(p.value); } return false; } @SuppressWarnings("unchecked") public int compareTo(Pair p) { int v = key.compareTo(p.key); if(v==0) { if(p.value instanceof Comparable) { return ((Comparable)value).compareTo(p.value); } } return v; } @Override public int hashCode() { return key.hashCode()^value.hashCode(); } @Override public String toString() { return key+": "+value; }} public class LRUCache<K extends Comparable, V> implements Cache<K, V>, Serializable { private static final long serialVersionUID = 3674312987828041877L; Map<K, Item> m_map = Collections.synchronizedMap(new HashMap<K, Item>()); Item m_start = new Item(); //表頭 Item m_end = new Item(); //表尾 int m_maxSize; Object m_listLock = new Object(); //用於並發的鎖 static class Item { public Item(Comparable k, Object v, long e) { key = k; value = v; expires = e; } public Item() {} public Comparable key; //索引值 public Object value; //對象 public long expires; //有效期間 public Item previous; public Item next; } void removeItem(Item item) { synchronized(m_listLock) { item.previous.next = item.next; item.next.previous = item.previous; } } void insertHead(Item item) { synchronized(m_listLock) { item.previous = m_start; item.next = m_start.next; m_start.next.previous = item; m_start.next = item; } } void moveToHead(Item item) { synchronized(m_listLock) { item.previous.next = item.next; item.next.previous = item.previous; item.previous = m_start; item.next = m_start.next; m_start.next.previous = item; m_start.next = item; } } public LRUCache(int maxObjects) { m_maxSize = maxObjects; m_start.next = m_end; m_end.previous = m_start; } @SuppressWarnings("unchecked") public Pair[] getAll() { Pair p[] = new Pair[m_maxSize]; int count = 0; synchronized(m_listLock) { Item cur = m_start.next; while(cur!=m_end) { p[count] = new Pair(cur.key, cur.value); ++count; cur = cur.next; } } Pair np[] = new Pair[count]; System.arraycopy(p, 0, np, 0, count); return np; } @SuppressWarnings("unchecked") public V get(K key) { Item cur = m_map.get(key); if(cur==null) { return null; } //到期則刪除對象 if(System.currentTimeMillis()>cur.expires) { m_map.remove(cur.key); removeItem(cur); return null; } if(cur!=m_start.next) { moveToHead(cur); } return (V)cur.value; } public void put(K key, V obj) { put(key, obj, -1); } public void put(K key, V value, long validTime) { Item cur = m_map.get(key); if(cur!=null) { cur.value = value; if(validTime>0) { cur.expires = System.currentTimeMillis()+validTime; } else { cur.expires = Long.MAX_VALUE; } moveToHead(cur); //成為最新的對象,移動到頭部 return; } if(m_map.size()>=m_maxSize) { cur = m_end.previous; m_map.remove(cur.key); removeItem(cur); } long expires=0; if(validTime>0) { expires = System.currentTimeMillis()+validTime; } else { expires = Long.MAX_VALUE; } Item item = new Item(key, value, expires); insertHead(item); m_map.put(key, item); } public void remove(K key) { Item cur = m_map.get(key); if(cur==null) { return; } m_map.remove(key); removeItem(cur); } public int size() { return m_map.size(); }}