Java实现LRUCache
前言
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
要求:你是否可以在 O(1) 时间复杂度内完成这两种操作?
LRUCache是最近最少使用缓存,具体概念不了解的可以自行百度,本篇是java代码的实现
提示:难点在于O(1) 时间复杂度,思路就是Hash表加双向链表,所以基本功一定要扎实。
要熟练掌握双向链表的头插和尾插,删除末尾和头部元素,删除某个节点
本题用虚拟头尾节点的方式
一、LRU实现
代码如下(示例):
LRUCache代码
class LRUCache {
public int capacity;//容量
public int size;//当前元素数量
public DNodeList head;
public DNodeList tail;
public Map<Integer,DNodeList> cache=new HashMap<>();;
class DNodeList {
int key;
int value;
DNodeList prev;
DNodeList next;
DNodeList(){
}
DNodeList(int key,int value){
this.key=key;
this.value=value;
this.prev=null;
this.next=null;
}
}
public LRUCache(int capacity) {
this.capacity=capacity;
this.size=0;
head = new DNodeList();
tail = new DNodeList();
head.next=tail;
tail.prev=head;
}
public int get(int key) {
//从cache里拿
DNodeList node=cache.get(key);
if(node==null){
return -1;
}
moveToHead(node);
return node.value;
}
public void moveToHead(DNodeList node){
//先删除
removeNode(node);
//再添加到头部
addHead(node);
}
public void put(int key,int value){
//先判断cache里有没有
DNodeList node=cache.get(key);
if(node==null){
DNodeList newNode=new DNodeList(key,value);
addHead(newNode);
cache.put(key,newNode);
++size;
//代表不存在,先判断是否大于容量
if(size>capacity){
//删除双向链表尾部的值,并更新cache
DNodeList temp=removeTail();
cache.remove(temp.key);
--size;
}
}else{
//存在改变原值,并放到头部
node.value=value;
moveToHead(node);
}
}
public void removeNode(DNodeList node){
node.prev.next=node.next;
node.next.prev=node.prev;
}
public void addHead(DNodeList node){
node.prev=head;
node.next=head.next;
head.next.prev=node;
head.next=node;
}
public DNodeList removeTail(){
DNodeList res= tail.prev;
removeNode(res);
return res;
}
}
补充
随着写代码越来越多,越是发现数据结构与算法这门内功是很有必要的,这里补充下尾插法的双向链表package algorithm.list;
public class DoubleLinkListTailInsert {
private Node head;
private Node tail;
public DoubleLinkListTailInsert() {
this.head = null;
this.tail = null;
}
//一定要心中有图,尾部插入
public void addLast(int data){
Node node=new Node(data);
//插入尾部。先固定个头节点
if(tail==null){
head=node;
}else{
tail.next=node;
node.prev=tail;
}
tail=node;
}
public void removeLast(){
if(tail==null){
new Exception("无此元素");
}else if(tail.prev==null){//就一个元素
head=null;
}else{ //断掉尾节点的前面的后继指针
tail.prev.next=null;
}
//尾指针向前移
tail=tail.prev;
}
public Node deleteNode(int data){
//遍历
Node current=tail;
while (current.data!=data){
if(current.prev==null){
System.out.println("无此元素");
return null;
}
current=current.prev;
}
//找到后进行删除,当前节点是尾节点,直接调用removeLast
if(current==tail){
removeLast();
}else{
current.prev.next = current.next;
if(current==head){
head=current.next;
current.next=null;
}else{
current.next.prev=current.prev;
}
}
return new Node(data);
}
public static void main(String[] args) {
DoubleLinkListTailInsert dl=new DoubleLinkListTailInsert();
dl.addLast(1);
dl.addLast(2);
dl.addLast(3);
dl.deleteNode(2);
dl.addLast(2);
}
}
class Node{
public int data;
public Node prev;
public Node next;
public Node(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
二、Redis LRU算法实现
分析Redis LRU实现之前,我们先了解一下Redis缓存淘汰策略。
当Redis内存超出物理内存限制时,内存会频繁的磁盘swap区交换数据,而交换会导致redis对外服务性能的急剧下降,这在生产环境是不允许的。说得更明白些,在生产环境是不允许交换行为的,通过设置maxmemory可限制内存超过期望大小。
当实际需要的内存大于maxmemory时,Redis提供了6种可选策略:
- noeviction:不继续提供写服务,读请求可以继续。
- volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰。也就是说没有设置过期时间的key不会被淘汰。
- volatile-ttl:也是淘汰设置了过期时间的key,只不过策略不是lru,而是根据剩余寿命的ttl值,ttl越小越优先被淘汰。
- volatile-random:同理,也是淘汰设置了过期时间的key,只不过策略是随机。
- allkeys-lru:类比volatile-lru,只不过未设置过期时间的key也在淘汰范围。
- allkeys-random:类比volatile-random,只不过未设置过期时间的key也在淘汰范围。
采用HashMap + 双向循环链表具有较好的读写性能,但是有没有发现什么问题呢?对,HashMap和链表都存在空间浪费的情况,HashMap本来就很耗内存,双向链表由于需要空间存储指针,两种数据结构空间使用率都不高,这显然很不划算。
咱们来看下Redis是如何实现的。Redis做法很简单:
随机取若干个key,然后按照访问时间排序,淘汰掉最不经常使用的数据。为此,Redis给每个key额外增加了一个24bit长度的字段,用于保存最后一次被访问的时钟(Redis维护了一个全局LRU时钟lruclock:REDIS_LUR_BITS,时钟分辨率默认1秒)。
redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key