Efficient cache management: Java implements LRU elimination algorithm

1. Introduction to LRU

LRU, the full name is Least Recently Used, is a cache elimination strategy. When storing data in the cache, if the cache fills up, some data needs to be evicted to make room. The LRU algorithm believes that recently used data with low frequency should be eliminated to retain hot data and improve the cache hit rate.

The LRU algorithm is usually implemented by maintaining a doubly linked list and a hash table. The hash table stores the key value of the data and the position of the corresponding node in the doubly linked list. The head of the linked list is the most recently used node, and the tail is the longest unused node. When new data is accessed, first check whether the node already exists in the hash table. If it exists, move the node to the head of the linked list. If it does not exist, create a new node and add it to the head of the linked list. At the same time, in the hash table Add key-value pairs to the hash table. When the cache is full, the node at the end of the linked list is removed and the corresponding key-value pair is deleted from the hash table.

The advantage of the LRU algorithm is that it is simple to implement, easy to understand and maintain, and is suitable for a variety of scenarios, such as caches, databases, etc. The disadvantage is that in certain scenarios the performance may not be as good as other elimination strategies, such as the LFU algorithm. In addition, the LRU algorithm may experience "jitter" when processing hotspot data sets, that is, data moves frequently between hotspots and non-hotspots, resulting in a decrease in cache hit rate. To address this problem, some improved LRU algorithms, such as 2Q algorithm, ARC algorithm, etc., have been proposed.

2. Common cache elimination algorithms

Common cache elimination algorithms include the following:

  • First-in-first-out (FIFO): According to the order in which data enters the cache, first-in first-out eliminates data.

  • Least Recently Used (LRU): Based on the age of the data in the cache, the least recently used data is eliminated. That is, data that has not been accessed in the recent period will be eliminated first.

  • Least Frequently Used (LFU): Based on the number of uses of the data in the cache, the data with the least usage is eliminated.

  • Time wheel algorithm (TTL): Based on the time-to-live (TTL) of the data in the cache, the data is eliminated after it expires.

  • Random algorithm: randomly eliminate some data.

Each of the above algorithms has advantages and disadvantages, and the appropriate elimination strategy should be selected based on the actual situation. For example, the FIFO algorithm is easy to implement, but it is not smart enough and will accidentally eliminate some more important data; while the LRU algorithm is more intelligent, but the implementation complexity is relatively high.

3. Java implements LRU

3.1. Define a doubly linked list node class

/**
 * <h1>定义一个双向链表节点类</h1>
 * 包含一个 key 和一个 value 属性,用于存储键值对。
 * */
public class ListNode {
    
    
    public Integer key;
    
    public Object value;

    public ListNode prev;

    public ListNode next;

    // 构造方法
    public ListNode(int key, Object value) {
    
    
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}

3.2. Define a doubly linked list class

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

/**
 * <h1>定义一个双向链表类</h1>
 * 包含一个头节点和一个尾节点,用于存储双向链表中的数据。
 * */
public class LRUCache {
    
    
    // 定义缓存容量
    private final int capacity;
    // 定义一个 HashMap 用于快速定位元素
    private final Map<Integer, ListNode> cache;
    // 定义头结点和尾节点
    private ListNode head, tail;

    /**
     * <h2>构造方法</h2>
     * */
    public LRUCache(int capacity) {
    
    
        // 初始化容量
        this.capacity = capacity;
        cache = new HashMap<>(capacity);

        // 初始化头结点和尾节点,并让它们相互指向
        this.head = new ListNode(-1, -1);
        this.tail = new ListNode(-1, -1);
        this.head.next = tail;
        this.tail.prev = head;
    }

    /**
     * <h2>将指定结点移到头结点之后</h2>
     * */
    private void moveNodeToHead(ListNode node) {
    
    
        // 如果 node 本来就是头结点,直接返回
        if (node == head.next) {
    
    
            return;
        }
        // 如果 node 不是尾节点,将 node 从双向链表中删除
        if (node.next != null && node.prev != null) {
    
    
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
        // 将node插入到头结点之后
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    /**
     * <h2>获取 key 对应的 value</h2>
     * */
    public Object getValue(int key) {
    
    
        // 如果 map 中没有该 key,返回-1
        if (!cache.containsKey(key)) {
    
    
            return -1;
        }
        // 将对应结点移到头结点之后,并返回对应 value
        ListNode node = cache.get(key);
        moveNodeToHead(node);
        return node.value;
    }

    /**
     * <h2>添加元素</h2>
     * */
    public void putValue(Integer key, Object value) {
    
    
        // 如果 key 已经存在,将对应结点移到头结点之后,更新 value
        if (cache.containsKey(key)) {
    
    
            ListNode node = cache.get(key);
            moveNodeToHead(node);
            node.value = value;
        }
        // 如果 key 不存在,新建一个结点,添加到头结点之后,并将其加入到 map 中
        else {
    
    
            ListNode newNode = new ListNode(key, value);
            cache.put(key, newNode);
            newNode.next = head.next;
            newNode.prev = head;
            head.next.prev = newNode;
            head.next = newNode;
            // 如果容量已经达到上限,将尾节点之前的结点删除
            if (cache.size() > capacity) {
    
    
                ListNode lastNode = tail.prev;
                cache.remove(lastNode.key);
                lastNode.prev.next = tail;
                tail.prev = lastNode.prev;
            }
        }
    }

    /**
     * <h2>打印 LRU 缓存信息</h2>
     * */
    public String printLog() {
    
    
        String result = "";
        ListNode node = this.head.next;
        while( node.next != null ) {
    
    
            result += node.key + " = " + node.value;
            if( node.next.key != -1) {
    
    
                result += ", ";
            }
            node = node.next;
        }
        return result;
    }
}

3.3. Define a unit test class

/**
 * <h1>定义一个单元测试类</h1>
 * */
public class LRUTest {
    
    

    public static void main(String[] args) {
    
    
        LRUCache lruCache = new LRUCache(5);

        System.out.println("初始状态:" + lruCache.printLog());

        lruCache.putValue(1, "V1");
        System.out.println("新增键 1:" + lruCache.printLog());

        lruCache.putValue(2, "V2");
        System.out.println("新增键 2:" +lruCache.printLog());

        lruCache.putValue(3, "V3");
        System.out.println("新增键 3:" +lruCache.printLog());

        lruCache.getValue(1);
        System.out.println("查询键 1:" +lruCache.printLog() + ",注意:链表数据位置变化");

        lruCache.putValue(4, "V4");
        System.out.println("新增键 4:" +lruCache.printLog());

        lruCache.putValue(5, "V5");
        System.out.println("新增键 5:" +lruCache.printLog());

        lruCache.getValue(4);
        System.out.println("查询键 4:" +lruCache.printLog() + ",注意:链表数据位置变化");

        lruCache.putValue(6, "V6");
        System.out.println("新增键 6:" +lruCache.printLog());

        System.out.println("最终状态:" + lruCache.printLog());
    }
}

Unit test output results

初始状态:
新增键 11 = V1
新增键 22 = V2, 1 = V1
新增键 33 = V3, 2 = V2, 1 = V1
查询键 11 = V1, 3 = V3, 2 = V2,注意:链表数据位置变化
新增键 44 = V4, 1 = V1, 3 = V3, 2 = V2
新增键 55 = V5, 4 = V4, 1 = V1, 3 = V3, 2 = V2
查询键 44 = V4, 5 = V5, 1 = V1, 3 = V3, 2 = V2,注意:链表数据位置变化
新增键 66 = V6, 4 = V4, 5 = V5, 1 = V1, 3 = V3
最终状态:6 = V6, 4 = V4, 5 = V5, 1 = V1, 3 = V3

This implementation uses a doubly linked list and a HashMap to implement LRU caching. HashMap is used to quickly find nodes, and doubly linked list is used to maintain the node order to implement the LRU strategy.

In this implementation, the LRUCache type cache has a fixed capacity, and when the cache reaches the capacity limit, the least recently used node is removed. When getting a node using the getValue method, if the node exists, it will be moved to the head of the linked list to indicate that it is the most recently used node. When adding a node using the putValue method, if the node already exists, its value will be updated and moved to the head of the linked list. If the node does not exist, a new node is added to the head of the linked list. If the cache exceeds the capacity limit after being added, the last node will be removed.

This is the basic idea of ​​the LRU algorithm: the use of data in the cache is constantly changing, and the cache size is limited, so a certain elimination strategy is needed to ensure the validity and timeliness of the data in the cache.

In general, the LRU algorithm is a relatively simple and efficient cache elimination strategy. It can quickly determine the least recently used data and eliminate it in time, thus ensuring the validity and timeliness of the data in the cache. In actual development, we can implement the LRU algorithm by combining different data structures and algorithms according to specific scenarios and needs.

This tutorial ends here. I wish you all can happily explore, learn, and grow in your programming journey!

Guess you like

Origin blog.csdn.net/u011374856/article/details/130152559