java 基于冷热数据分离的思想设计LRU链表
1. LRUCache 伪代码
import java.util.HashMap;
import java.util.Map;public class LRUCache {private final int capacity; // 缓存的最大容量private final Map<Integer, Node> map; // 用于快速查找节点的哈希表private final Node headHot, tailHot; // 热数据链表的头节点和尾节点private final Node headCold, tailCold; // 冷数据链表的头节点和尾节点private int hotSize, coldSize; // 热数据和冷数据的数量public LRUCache(int capacity) {this.capacity = capacity; // 初始化缓存容量this.map = new HashMap<>(); // 初始化哈希表this.headHot = new Node(0, 0); // 初始化热数据链表的头节点this.tailHot = new Node(0, 0); // 初始化热数据链表的尾节点this.headCold = new Node(0, 0); // 初始化冷数据链表的头节点this.tailCold = new Node(0, 0); // 初始化冷数据链表的尾节点headHot.next = tailHot; // 将头节点和尾节点连接起来tailHot.prev = headHot;headCold.next = tailCold; // 将头节点和尾节点连接起来tailCold.prev = headCold;this.hotSize = 0; // 初始化热数据数量为0this.coldSize = 0; // 初始化冷数据数量为0}public int get(int key) {Node node = map.get(key); // 从哈希表中查找节点if (node == null) return -1; // 如果节点不存在,返回-1if (node.isHot) {// 如果节点是热数据,将其移动到热数据链表头部removeNode(node);addToHeadHot(node);} else {// 如果节点是冷数据,将其移动到冷数据链表头部removeNode(node);addToHeadCold(node);// 如果冷数据链表的大小超过容量的一半,移除最久未使用的冷数据if (coldSize > capacity / 2) {Node lruCold = tailCold.prev;removeNode(lruCold);map.remove(lruCold.key);coldSize--;// 如果热数据链表的大小小于容量的一半,将最久未使用的冷数据提升为热数据if (hotSize < capacity / 2) {addToHeadHot(lruCold);lruCold.isHot = true;hotSize++;}}}return node.value; // 返回节点的值}public void put(int key, int value) {if (capacity == 0) return; // 如果缓存容量为0,直接返回Node node = map.get(key); // 从哈希表中查找节点if (node != null) {// 如果节点存在,更新其值node.value = value;if (node.isHot) {// 如果节点是热数据,将其移动到热数据链表头部removeNode(node);addToHeadHot(node);} else {// 如果节点是冷数据,将其移动到冷数据链表头部removeNode(node);addToHeadCold(node);// 如果冷数据链表的大小超过容量的一半,移除最久未使用的冷数据if (coldSize > capacity / 2) {Node lruCold = tailCold.prev;removeNode(lruCold);map.remove(lruCold.key);coldSize--;// 如果热数据链表的大小小于容量的一半,将最久未使用的冷数据提升为热数据if (hotSize < capacity / 2) {addToHeadHot(lruCold);lruCold.isHot = true;hotSize++;}}}} else {// 如果节点不存在,创建新节点if (hotSize + coldSize >= capacity) {// 如果缓存已满,移除最久未使用的节点if (coldSize > capacity / 2) {Node lruCold = tailCold.prev;removeNode(lruCold);map.remove(lruCold.key);coldSize--;} else {Node lruHot = tailHot.prev;removeNode(lruHot);map.remove(lruHot.key);hotSize--;}}Node newNode = new Node(key, value); // 创建新节点addToHeadHot(newNode); // 将新节点插入到热数据链表头部map.put(key, newNode); // 将新节点添加到哈希表中hotSize++; // 更新热数据数量}}private void addToHeadHot(Node node) {node.isHot = true; // 设置节点为热数据node.prev = headHot; // 将节点插入到热数据链表头部node.next = headHot.next;headHot.next.prev = node;headHot.next = node;hotSize++; // 更新热数据数量}private void addToHeadCold(Node node) {node.isHot = false; // 设置节点为冷数据node.prev = headCold; // 将节点插入到冷数据链表头部node.next = headCold.next;headCold.next.prev = node;headCold.next = node;coldSize++; // 更新冷数据数量}private void removeNode(Node node) {if (node.isHot) {hotSize--; // 如果节点是热数据,更新热数据数量} else {coldSize--; // 如果节点是冷数据,更新冷数据数量}node.prev.next = node.next; // 从链表中移除节点node.next.prev = node.prev;}private static class Node {int key; // 节点的键int value; // 节点的值boolean isHot; // 节点是否为热数据Node prev; // 前驱节点Node next; // 后继节点Node(int key, int value) {this.key = key;this.value = value;}}
}
说明
数据结构:
使用双向链表来存储热数据和冷数据。
使用哈希表来快速查找节点。
操作:
get(int key): 如果节点存在且是热数据,则将其移动到热数据链表头部;如果是冷数据,则将其移动到冷数据链表头部,并根据冷数据链表的大小决定是否将其提升为热数据。
put(int key, int value): 如果节点存在,则更新其值并根据其状态移动到相应链表的头部;如果节点不存在,则创建新节点并插入到热数据链表头部,同时根据缓存容量决定是否需要移除最久未使用的节点。
冷热数据分离:
通过维护两个链表分别存储热数据和冷数据,可以有效地分离冷热数据。
当冷数据链表的大小超过缓存容量的一半时,会将最久未使用的冷数据移除,并根据热数据链表的大小决定是否将其提升为热数据。
这种设计可以提高缓存的命中率和访问速度,特别是在访问模式存在明显冷热数据分离的情况下。
2. LRU 的单元测试
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;public class LRUCacheTest {private LRUCache cache;@BeforeEachpublic void setUp() {cache = new LRUCache(3); // 初始化缓存容量为3}@Testpublic void testPutAndGet() {cache.put(1, 1);cache.put(2, 2);cache.put(3, 3);assertEquals(1, cache.get(1)); // 测试正常获取assertEquals(2, cache.get(2)); // 测试正常获取assertEquals(3, cache.get(3)); // 测试正常获取}@Testpublic void testEviction() {cache.put(1, 1);cache.put(2, 2);cache.put(3, 3);cache.put(4, 4); // 触发淘汰assertEquals(-1, cache.get(1)); // 测试淘汰最久未使用的元素assertEquals(2, cache.get(2)); // 测试正常获取assertEquals(3, cache.get(3)); // 测试正常获取assertEquals(4, cache.get(4)); // 测试正常获取}@Testpublic void testUpdate() {cache.put(1, 1);cache.put(2, 2);cache.put(1, 10); // 更新值assertEquals(10, cache.get(1)); // 测试更新后的值assertEquals(2, cache.get(2)); // 测试未更新的值}@Testpublic void testGetUpdatesOrder() {cache.put(1, 1);cache.put(2, 2);cache.put(3, 3);cache.get(1); // 访问1,变为最近使用cache.put(4, 4); // 触发淘汰assertEquals(1, cache.get(1)); // 测试正常获取assertEquals(-1, cache.get(2)); // 测试淘汰最久未使用的元素assertEquals(3, cache.get(3)); // 测试正常获取assertEquals(4, cache.get(4)); // 测试正常获取}@Testpublic void testCapacityZero() {LRUCache zeroCapacityCache = new LRUCache(0);zeroCapacityCache.put(1, 1);assertEquals(-1, zeroCapacityCache.get(1)); // 测试容量为0时无法存储元素}@Testpublic void testColdToHot() {cache.put(1, 1);cache.put(2, 2);cache.put(3, 3);cache.get(1); // 访问1,变为最近使用cache.get(2); // 访问2,变为最近使用cache.get(3); // 访问3,变为最近使用cache.put(4, 4); // 触发淘汰cache.put(5, 5); // 触发淘汰assertEquals(1, cache.get(1)); // 测试正常获取assertEquals(2, cache.get(2)); // 测试正常获取assertEquals(3, cache.get(3)); // 测试正常获取assertEquals(-1, cache.get(4)); // 测试淘汰最久未使用的元素assertEquals(5, cache.get(5)); // 测试正常获取}@Testpublic void testHotToCold() {cache.put(1, 1);cache.put(2, 2);cache.put(3, 3);cache.get(1); // 访问1,变为最近使用cache.get(2); // 访问2,变为最近使用cache.get(3); // 访问3,变为最近使用cache.put(4, 4); // 触发淘汰cache.get(1); // 访问1,变为最近使用cache.get(2); // 访问2,变为最近使用cache.put(5, 5); // 触发淘汰assertEquals(1, cache.get(1)); // 测试正常获取assertEquals(2, cache.get(2)); // 测试正常获取assertEquals(-1, cache.get(3)); // 测试淘汰最久未使用的元素assertEquals(4, cache.get(4)); // 测试正常获取assertEquals(5, cache.get(5)); // 测试正常获取}
}