如何设计一个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 实现:
1 public interface Cache<K extends Comparable, V> { 2 3 V get(K obj); //查询 4 5 void put(K key, V obj); //插入和更新 6 7 void put(K key, V obj, long validTime); 8 9 void remove(K key); //删除 10 11 Pair[] getAll(); 12 13 int size(); 14 15 } 16 17 public class Pair<K extends Comparable, V> implements Comparable<Pair> { 18 19 public Pair(K key1, V value1) { 20 21 this.key = key1; 22 23 this.value = value1; 24 25 } 26 27 public K key; 28 29 public V value; 30 31 public boolean equals(Object obj) { 32 33 if(obj instanceof Pair) { 34 35 Pair p = (Pair)obj; 36 37 return key.equals(p.key)&&value.equals(p.value); 38 39 } 40 41 return false; 42 43 } 44 45 @SuppressWarnings("unchecked") 46 47 public int compareTo(Pair p) { 48 49 int v = key.compareTo(p.key); 50 51 if(v==0) { 52 53 if(p.value instanceof Comparable) { 54 55 return ((Comparable)value).compareTo(p.value); 56 57 } 58 59 } 60 61 return v; 62 63 } 64 65 @Override 66 67 public int hashCode() { 68 69 return key.hashCode()^value.hashCode(); 70 71 } 72 73 @Override 74 75 public String toString() { 76 77 return key+": "+value; 78 79 } 80 81 } 82 83 public class LRUCache<K extends Comparable, V> implements Cache<K, V>, 84 85 Serializable { 86 87 private static final long serialVersionUID = 3674312987828041877L; 88 89 Map<K, Item> m_map = Collections.synchronizedMap(new HashMap<K, Item>()); 90 91 Item m_start = new Item(); //表头 92 93 Item m_end = new Item(); //表尾 94 95 int m_maxSize; 96 97 Object m_listLock = new Object(); //用于并发的锁 98 99 static class Item { 100 101 public Item(Comparable k, Object v, long e) { 102 103 key = k; 104 105 value = v; 106 107 expires = e; 108 109 } 110 111 public Item() {} 112 113 public Comparable key; //键值 114 115 public Object value; //对象 116 117 public long expires; //有效期 118 119 public Item previous; 120 121 public Item next; 122 123 } 124 125 void removeItem(Item item) { 126 127 synchronized(m_listLock) { 128 129 item.previous.next = item.next; 130 131 item.next.previous = item.previous; 132 133 } 134 135 } 136 137 void insertHead(Item item) { 138 139 synchronized(m_listLock) { 140 141 item.previous = m_start; 142 143 item.next = m_start.next; 144 145 m_start.next.previous = item; 146 147 m_start.next = item; 148 149 } 150 151 } 152 153 void moveToHead(Item item) { 154 155 synchronized(m_listLock) { 156 157 item.previous.next = item.next; 158 159 item.next.previous = item.previous; 160 161 item.previous = m_start; 162 163 item.next = m_start.next; 164 165 m_start.next.previous = item; 166 167 m_start.next = item; 168 169 } 170 171 } 172 173 public LRUCache(int maxObjects) { 174 175 m_maxSize = maxObjects; 176 177 m_start.next = m_end; 178 179 m_end.previous = m_start; 180 181 } 182 183 @SuppressWarnings("unchecked") 184 185 public Pair[] getAll() { 186 187 Pair p[] = new Pair[m_maxSize]; 188 189 int count = 0; 190 191 synchronized(m_listLock) { 192 193 Item cur = m_start.next; 194 195 while(cur!=m_end) { 196 197 p[count] = new Pair(cur.key, cur.value); 198 199 ++count; 200 201 cur = cur.next; 202 203 } 204 205 } 206 207 Pair np[] = new Pair[count]; 208 209 System.arraycopy(p, 0, np, 0, count); 210 211 return np; 212 213 } 214 215 @SuppressWarnings("unchecked") 216 217 public V get(K key) { 218 219 Item cur = m_map.get(key); 220 221 if(cur==null) { 222 223 return null; 224 225 } 226 227 //过期则删除对象 228 229 if(System.currentTimeMillis()>cur.expires) { 230 231 m_map.remove(cur.key); 232 233 removeItem(cur); 234 235 return null; 236 237 } 238 239 if(cur!=m_start.next) { 240 241 moveToHead(cur); 242 243 } 244 245 return (V)cur.value; 246 247 } 248 249 public void put(K key, V obj) { 250 251 put(key, obj, -1); 252 253 } 254 255 public void put(K key, V value, long validTime) { 256 257 Item cur = m_map.get(key); 258 259 if(cur!=null) { 260 261 cur.value = value; 262 263 if(validTime>0) { 264 265 cur.expires = System.currentTimeMillis()+validTime; 266 267 } 268 269 else { 270 271 cur.expires = Long.MAX_VALUE; 272 273 } 274 275 moveToHead(cur); //成为最新的对象,移动到头部 276 277 return; 278 279 } 280 281 if(m_map.size()>=m_maxSize) { 282 283 cur = m_end.previous; 284 285 m_map.remove(cur.key); 286 287 removeItem(cur); 288 289 } 290 291 long expires=0; 292 293 if(validTime>0) { 294 295 expires = System.currentTimeMillis()+validTime; 296 297 } 298 299 else { 300 301 expires = Long.MAX_VALUE; 302 303 } 304 305 Item item = new Item(key, value, expires); 306 307 insertHead(item); 308 309 m_map.put(key, item); 310 311 } 312 313 public void remove(K key) { 314 315 Item cur = m_map.get(key); 316 317 if(cur==null) { 318 319 return; 320 321 } 322 323 m_map.remove(key); 324 325 removeItem(cur); 326 327 } 328 329 public int size() { 330 331 return m_map.size(); 332 333 } 334 335 }