LRU是什么
缓存命中率是缓存系统的非常重要指标,如果缓存系统的缓存命中率过低,将会导致查询回流到数据库,导致数据库的压力升高。
LRU(Least Recently Used) 即最近最少使用的数据需要被淘汰,属于典型的内存淘汰机制。也就是说,内存中淘汰那些最近最少使用的数据。
LRU算法实现思路
根据LRU算法的理念,我们需要:
一个参数来作为容量阈值
一种数据结构来存储数据,同时希望插入、读取、删除操作的时间复杂度都是O(1)。
所以,我们用到的数据结构是:Hashmap+双向链表。
1.利用hashmap的get、put方法O(1)的时间复杂度,快速取、存数据。
2.利用doublelinkedlist的特征(可以访问到某个节点之前和之后的节点),实现O(1)的新增和删除数据。
如下图所示:
LRU的简单实现
节点node,存放key、val值、前节点、后节点
class Node{
public int key;
public int val;
public Node next;
public Node previous;
public Node() {
}
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
双向链表,属性有size、头节点、尾节点。
提供api:
class DoubleList{
private int size;
private Node head;
private Node tail;
public DoubleList() {
this.head = new Node();
this.tail = new Node();
size = 0;
head.next = tail;
tail.previous = head;
}
public void addFirst(Node node){
Node temp = head.next;
head.next = node;
node.previous = head;
node.next = temp;
temp.previous = node;
size++;
}
public void remove(Node node){
if(null==node|| node.previous==null|| node.next==null){
return;
}
node.previous.next = node.next;
node.next.previous = node.previous;
node.next=null;
node.previous=null;
size--;
}
public void remove(){
if(size<=0) return;
Node temp = tail.previous;
temp.previous.next = temp.next;
tail.previous = temp.previous;
temp.next = null;
temp.previous=null;
size--;
}
public int size(){
return size;
}
}
LRU算法实现类
API
public class LRUCache {
Map
DoubleList cache;
int cap;
public LRUCache(int cap) {
map = new HashMap<>();
cache = new DoubleList();
this.cap = cap;
}
public int get(int key){
Node node = map.get(key);
return node==null? -1:node.val;
}
public void put(int key, int val){
Node node = new Node(key,val);
if(map.get(key)!=null){
cache.remove(map.get(key));
cache.addFirst(node);
map.put(key,node);
return;
}
map.put(key,node);
cache.addFirst(node);
if(cache.size()>cap){
cache.remove();
}
}
public static void main(String[] args) {
//test, cap = 3
LRUCache lruCache = new LRUCache(3);
lruCache.put(1,1);
lruCache.put(2,2);
lruCache.put(3,3);
//<1,1>来到链表头部
lruCache.put(1,1);
//<4,4>来到链表头部, <2,2>被淘汰。
lruCache.put(4,4);
}
}
LRU应用场景
LRU 算法劣势在于对于偶发的批量操作,比如说批量查询历史数据,就有可能使缓存中热门数据被这些历史数据替换,造成缓存污染,导致缓存命中率下降,减慢了正常数据查询。
扩展
1.LRU-K
LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
LRU-K具有LRU的优点,同时还能避免LRU的缺点,实际应用中LRU-2是综合最优的选择。由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多。
2.Two queue
Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。 当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。
新访问的数据插入到FIFO队列中,如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;如果数据在FIFO队列中再次被访问到,则将数据移到LRU队列头部,如果数据在LRU队列中再次被访问,则将数据移动LRU队列头部,LRU队列淘汰末尾的数据。
3.MySQL InnoDB LRU
将链表拆分成两部分,分为热数据区,与冷数据区。
若该数据已在缓存中超过指定时间,比如说 1 s,则移动到热数据区的头结点。
若该数据存在在时间小于指定的时间,则位置保持不变。
对于偶发的批量查询,数据仅仅只会落入冷数据区,然后很快就会被淘汰出去。热门数据区的数据将不会受到影响,这样就解决了 LRU 算法缓存命中率下降的问题。