Redis内存淘汰策略中会使用到 LRU 算法,以下是简单说明及Java代码实现。

一、前言

LRU(The Least Recently Used)即最近最少使用算法。其原理是如果一个数据最近没有被使用,那么将来它被使用的可能也更低,因此在内存达到一定阈值时,将最近最少使用的数据淘汰的一种策略算法。

二、图解

TODO 图解说明后期有空再补充

三、LRU 算法 Java 实现

Java实现LRU算法可通过哈希表+双向链表的方式实现,在哈希表中维护所有节点,实现查找的时间复杂度为O(1)(实际元素出现哈希碰撞后,形成的链表或者红黑树时间复杂度为O(logN)此处不考虑,只当哈希表为O(1)))。

3.1 定义节点类

节点Node类中应包含 key(键)、value(值)和prev(前置节点)、next(后置节点)指针

import lombok.Getter;
import lombok.Setter;

/**
 * 节点Node
 */
@Getter
@Setter
public class Node {

    /**
     * 节点键key
     */
    private int key;

    /**
     * 节点值value
     */
    private int value;

    /**
     * 前置节点
     */
    private Node prev;

    /**
     * 后置节点
     */
    private Node next;

    public Node() {
    }

    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }

}

注意:代码中 @Getter、@Setter 使用的是 lombok 插件,如果没有安装直接删除这两个注解和对应 import 导入逻辑,再手工给所有属性添加 Getter/Setter 方法即可,后续代码也是不做另外说明;

3.2 双向链表实现

双向链表中定义 dummyHead(虚拟头节点)、dummyTail(虚拟尾节点)两个虚拟头尾节点,避免后续处理中针对头尾节点的特殊处理;再用 length 属性维护链表长度

真实头节点:dummyHead.next
真实尾节点:dummyTail.prev

双向链表中定义 add、delete、deleteHead 方法,实现对链表的新增、删除及删除头节点功能(因为我们新增插入是在链表尾部,所以如需淘汰从链表头开始);

import lombok.Getter;
import lombok.Setter;

/**
 * 双向链表
 * LRU实现使用双向链表存储,头尾节点(head、tail)使用两个默认的虚拟节点,这样避免使用时针对头尾节点的特殊处理
 */
@Getter
@Setter
public class DoubleLinkedList {

    /**
     * 虚拟头节点,实际头节点应为dummyHead.next
     */
    private Node dummyHead;

    /**
     * 虚拟尾节点,实际尾节点应为dummyTail.prev
     */
    private Node dummyTail;

    /**
     * 链表长度
     */
    private int length;

    public DoubleLinkedList() {
        //默认虚拟头、尾节点初始化
        this.dummyHead = new Node();
        this.dummyTail = new Node();
        this.dummyHead.setNext(this.dummyTail);
        this.dummyTail.setPrev(this.dummyHead);
        this.length = 0;
    }

    /**
     * 添加节点
     * 每次添加节点在链表末尾
     * @param node
     */
    public void add(Node node) {
        node.setNext(this.dummyTail);
        node.setPrev(this.dummyTail.getPrev());
        this.dummyTail.getPrev().setNext(node);
        this.dummyTail.setPrev(node);
        this.length++;
    }

    /**
     * 删除节点
     * @param node
     */
    public void delete(Node node) {
        node.getPrev().setNext(node.getNext());
        node.getNext().setPrev(node.getPrev());
        node.setPrev(null);
        node.setNext(null);
        this.length--;
    }

    /**
     * 删除头节点
     */
    public void deleteHead() {
        Node head = this.dummyHead.getNext();
        if (head == null) {
            return;
        }
        this.delete(this.dummyHead.getNext());

    }

}

3.3 LRUCache 类实现

即使用我们上方定义的双向链表(DoubleLinkedList )+ 哈希表(HashMap)实现LRU。LRUCache 中定义哈希表 nodeMap(节点Map)、双向链表 linkedList、capacity(容量大小)属性,且应包含 get、put 方法,具体实现见下面代码

import java.util.HashMap;
import java.util.Map;

/**
 * LRU Java实现,指定容量,达到时淘汰最久未使用数据
 * 通过双向链表+哈希表的方式实现查找、插入、删除的复杂度为O(1)
 */
public class LRUCache {

    /**
     * 节点Map
     */
    private Map<Integer, Node> nodeMap;

    /**
     * 双向链表
     */
    private DoubleLinkedList linkedList;

    /**
     * 容量大小,超过时应淘汰最久未使用数据
     */
    private int capacity;

    /**
     * 元素不存在默认为-1
     */
    private static final int NOT_FOUND = -1;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        nodeMap = new HashMap<>();
        linkedList = new DoubleLinkedList();
    }

    /**
     * get获取单个节点值,未找到则返回-1
     * @param key
     * @return
     */
    public int get(int key) {
        if (this.capacity == 0) {
            //容量capacity设置为0直接返回-1
            return NOT_FOUND;
        }
        if (!nodeMap.containsKey(key)) {
            //根据key在哈希表中未找到即不存在返回-1
            return NOT_FOUND;
        }
        //节点存在
        Node node = nodeMap.get(key);
        //get获取某个节点,即最新使用应将节点移动到双向链表尾部,可认为是删除、插入链表的一个组合操作
        this.linkedList.delete(node);
        this.linkedList.add(node);
        return node.getValue();
    }

    /**
     * put新增或更新节点
     * 注意:新增节点时如达到设定容量上限,则应淘汰最久未使用数据
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        if (this.capacity == 0) {
            //容量capacity设置为0即认为无法进行put操作,直接返回即可
            return ;
        }

        if (nodeMap.containsKey(key)) {
            //键值对存在,即应更新维护键值对新值,且将该节点移动到链表尾部
            Node node = nodeMap.get(key);
            this.linkedList.delete(node);
        } else if (this.capacity == this.linkedList.getLength()) {
            //容量达到上限,删除链表中头节点,且从哈希表中也要移除映射
            Node head = this.linkedList.getDummyHead().getNext();
            nodeMap.remove(head.getKey());
            this.linkedList.deleteHead();
        }
        //重新维护哈希表映射,新增节点(默认移动到尾部)
        Node node = new Node(key, value);
        nodeMap.put(key, node);
        this.linkedList.add(node);
    }

    @Override
    public String toString() {
        if (this.linkedList.getLength() == 0) {
            return "LRUCache{}";
        }

        //从头节点开始遍历按顺序打印
        Node node = this.linkedList.getDummyHead().getNext();
        StringBuffer sb = new StringBuffer("LRUCache{");
        while (node != this.linkedList.getDummyTail()) {
            if (node.getNext() == this.linkedList.getDummyTail()) {
                //最后一个元素
                sb.append(node.getKey() + ": " + node.getValue());
            } else {
                sb.append(node.getKey() + ": " + node.getValue() + ", ");
            }
            node = node.getNext();
        }
        sb.append("}");
        return sb.toString();
    }
    
}

3.4 测试验证

测试代码如下:

public static void main(String[] args) {
        LRUCache LRUCache = new LRUCache(2);
        LRUCache.put(1, 11);
        LRUCache.put(2, 22);
        System.out.println(LRUCache);
        System.out.println("获取节点1,值为" + LRUCache.get(1));
        System.out.println(LRUCache);

        System.out.println("更新节点2,新值为250");
        LRUCache.put(2, 250);
        System.out.println(LRUCache);
        System.out.println("达到最大容量,插入新值");
        LRUCache.put(6, 66);
        System.out.println(LRUCache);
    }

打印输出如下:

LRUCache{1: 11, 2: 22}
获取节点1,值为11
LRUCache{2: 22, 1: 11}
更新节点2,新值为250
LRUCache{1: 11, 2: 250}
达到最大容量,插入新值
LRUCache{2: 250, 6: 66}

四、使用LinkedHashMap实现LRU

LinkedHashMap 内部本身就很好的支持了LRU算法,可通过继承 LinkedHashMap 实现 LRU,只要定义 capacity(容量大小),且覆盖父类 removeEldestEntry 方法,定义删除最早元素触发逻辑(this.size() > this.capacity)即可;

import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.Map;

@Getter
public class LRUCache<K, V> extends LinkedHashMap {

    /**
     * 容量大小
     */
    private int capacity;

    public LRUCache(int capacity) {
        super();
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        //链表长度达到上限,触发删除链表最早未使用元素
        return this.size() > this.capacity;
    }
    
}

Alex
1 声望0 粉丝