如何設計一個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對應的記錄
- 將新的節點插入到雙向鏈表中頭部
更新:
- 和查詢相似
刪除:
- 從雙向鏈表和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();
}
}