Redis 缓存淘汰策略
Redis 缓存淘汰策略是当内存使用达到上限时Redis 自动清理部分数据以腾出空间的核心机制官方共定义了 8 种主流策略分为两大类别一、8种淘汰策略分类针对所有键的策略noeviction默认内存满时拒绝写入新数据直接返回OOM错误读请求可正常执行。allkeys-lru从所有键中淘汰最近最少使用的数据是纯缓存场景的首选。allkeys-lfu从所有键中淘汰访问频次最低的数据能更精准保留热点数据。allkeys-random从所有键中随机淘汰部分键性能开销极低。仅针对设置了过期时间的键的策略volatile-lru仅从带过期时间的键中淘汰最近最少使用的数据保护永久数据。volatile-lfu仅从带过期时间的键中淘汰访问频次最低的数据。volatile-random仅从带过期时间的键中随机淘汰部分键。volatile-ttl优先淘汰剩余存活时间最短的键适配即将过期的数据清理场景。二、核心算法差异LRU基于访问时间维度优先淘汰长时间未被访问的数据适配冷热数据分界清晰的业务场景。LFU基于访问频次维度优先淘汰访问次数少的数据相比LRU能更稳定保留长期热点数据实现逻辑更复杂。三、配置与选型建议动态生效命令执行 config set maxmemory-policy allkeys-lru 即可实时切换策略。持久配置在Redis配置文件中修改 maxmemory-policy 参数重启后永久生效。选型参考纯缓存场景推荐allkeys-lru混合存储场景推荐volatile-lru避免误删未设置过期时间的核心业务数据。四LRU实现最简单方法实现 LRULeast Recently Used最近最少使用缓存最简单的方法取决于你的应用场景是工程落地还是面试/算法考察。工程落地最简单继承 LinkedHashMap (Java)在 Java 实际开发中无需手写链表直接利用 JDK 自带的 LinkedHashMap 即可快速实现一个线程非安全的 LRU 缓存。这是最简洁、最高效的工程实现方式。核心原理LinkedHashMap 内部维护了一个双向链表来记录插入顺序或访问顺序。通过构造函数开启“访问顺序模式”并重写 removeEldestEntry 方法即可自动淘汰最久未使用的数据。代码示例importjava.util.LinkedHashMap;importjava.util.Map;publicclassSimpleLRUCacheK,VextendsLinkedHashMapK,V{privatefinalintcapacity;publicSimpleLRUCache(intcapacity){// 第三个参数 true 表示按照访问顺序排序false 表示按照插入顺序排序super(capacity,0.75f,true);this.capacitycapacity;}OverrideprotectedbooleanremoveEldestEntry(Map.EntryK,Veldest){// 当地图大小超过指定容量时移除最老的条目returnsize()capacity;}}简单测试一下publicclassLRUTest{publicstaticvoidmain(String[]args){// 创建容量为 3 的 LRU 缓存LRUCacheInteger,StringcachenewLRUCache(3);// 1. 插入数据cache.put(1,A);cache.put(2,B);cache.put(3,C);System.out.println(初始状态: cache);// 输出: {1A, 2B, 3C}// 2. 访问 Key 1将其移至链表尾部变为最近使用cache.get(1);System.out.println(访问1后: cache);// 输出: {2B, 3C, 1A}// 3. 插入新数据 Key 4此时容量已满应淘汰最久未使用的 Key 2cache.put(4,D);System.out.println(插入4后: cache);// 输出: {3C, 1A, 4D}// 验证Key 2 已被淘汰返回 nullSystem.out.println(获取2: cache.get(2));// 输出: null}}关键细节说明accessOrder true这是灵魂参数。若设为 false默认链表仅按插入顺序排列无法体现“最近使用”也就无法实现 LRU。removeEldestEntry该方法在每次 put 操作后自动调用。返回 true 时LinkedHashMap 会自动移除双向链表头部的节点即 eldest。线程安全上述实现是非线程安全的。若需在多线程环境使用建议通过 Collections.synchronizedMap(new LRUCache(capacity)) 进行包装或在方法级别加锁。优点代码极少逻辑清晰JDK 原生支持性能可靠。缺点非线程安全多线程环境需加锁或使用 Collections.synchronizedMap且无法自定义更复杂的淘汰逻辑。2. 前端/脚本语言最简单使用 Map 手动维护顺序 (JavaScript/Python)在 JavaScript 或 Python 等动态语言中没有现成的“访问顺序 LinkedHashMap”最简单的实现是利用 Map 对象保证插入顺序配合删除和重新插入操作来模拟“最近使用移到头部”的逻辑。JavaScript 代码示例classLRUCache{constructor(capacity){this.capacitycapacity;this.cachenewMap();}get(key){if(!this.cache.has(key))return-1;// 关键步骤先删除再重新插入将其移到 Map 末尾代表最近使用constvaluethis.cache.get(key);this.cache.delete(key);this.cache.set(key,value);returnvalue;}put(key,value){if(this.cache.has(key)){this.cache.delete(key);// 如果存在先删除以更新位置}elseif(this.cache.sizethis.capacity){// 如果满了删除 Map 的第一个元素最久未使用constfirstKeythis.cache.keys().next().value;this.cache.delete(firstKey);}this.cache.set(key,value);// 插入到末尾}}优点逻辑直观利用了 Map 的有序特性无需实现复杂的链表节点结构。注意Map 的 keys().next().value 获取第一个键的时间复杂度在某些引擎中可能不是严格的 O(1)但在大多数现代 JS 引擎中性能足够好。3. 面试/算法考察标准解法HashMap 双向链表如果是在面试中被问到“如何实现 LRU”面试官通常希望看到你理解底层数据结构而不是调用库函数。此时“最简单”指的是逻辑最清晰的标准解法即组合使用 HashMap 和 双向链表。核心思路HashMap提供 O(1) 的查找能力Key 映射到双向链表的节点。双向链表维护数据的访问顺序。头部最近使用的数据。尾部最久未使用的数据。操作逻辑Get从 HashMap 找到节点将该节点移动到链表头部。Put若 Key 存在更新值并移动到链表头部。若 Key 不存在创建新节点放入头部。若容量已满删除链表尾部节点并从 HashMap 中移除对应 Key。为什么不用单向链表或数组数组移动元素需要大量拷贝时间复杂度 O(N)。单向链表删除节点时需要知道前驱节点查找前驱需要 O(N)无法实现 O(1) 删除。双向链表可以在 O(1) 时间内完成节点的删除和移动。总结建议实际项目直接用 LinkedHashMap (Java) 或 Caffeine 库不要重复造轮子。前端开发用 Map 模拟简单有效。面试笔试必须手写 HashMap 双向链表并注意处理头尾指针的边界情况建议使用虚拟头尾节点简化代码。五LinkedHashMap 实现LRU原理LinkedHashMap 实现 LRULeast Recently Used最近最少使用缓存的核心原理在于其内部维护了一套双向链表来记录元素的访问或插入顺序并结合 HashMap 的哈希查找能力从而在保证 O(1) 查找效率的同时能够以 O(1) 的时间复杂度维护数据的“新鲜度”。以下是其实现 LRU 的具体机制拆解数据结构基础HashMap 双向链表LinkedHashMap 继承自 HashMap因此它保留了 HashMap数组 单向链表/红黑树 的结构用于快速定位数据。在此基础上它额外增加了一个双向链表结构节点改造HashMap中的 Entry 节点在 LinkedHashMap中被扩展为包含 before 和 after 指针的双向链表节点。头尾哨兵内部维护一个特殊的 header 节点或在 Java 8 中通过 head 和 tail 引用形成一个循环双向链表。header.after或 head指向链表中最久未使用的节点。header.before或 tail指向链表中最近使用的节点。2. 两种排序模式LinkedHashMap 构造函数中有一个关键参数 accessOrderfalse默认插入顺序。元素按照放入 Map 的顺序排列新元素加到链表尾部。这种模式不体现 LRU 特性。true访问顺序。这是实现 LRU 的关键。每当调用 get() 或 put()更新已存在的 key时被操作的节点会被移动到双向链表的尾部即最近使用端。3. LRU 核心逻辑实现步骤A. 访问时移动节点保持新鲜度当 accessOrder 为 true 时Get 操作通过 HashMap 快速找到节点后调用内部方法将该节点从当前位置移除并重新链接到双向链表的尾部。Put 操作如果 Key 已存在更新 Value 后同样将该节点移动到链表尾部。这一过程确保了链表头部始终是最久未被访问的数据链表尾部始终是最近被访问的数据。B. 自动淘汰机制移除最旧数据为了实现缓存容量限制LinkedHashMap 提供了一个受保护的方法 removeEldestEntry(Map.EntryK,V eldest)。默认行为该方法默认返回 false即不删除任何元素。LRU 实现技巧用户只需继承 LinkedHashMap 并重写该方法当 size() capacity 时返回 true。触发时机每次执行 put 操作添加新元素后LinkedHashMap 会自动调用 removeEldestEntry。如果返回 true它会自动移除双向链表头部header.after的节点因为那里存放的就是最久未使用的数据。4. 代码示例通过极简的代码即可实现一个标准的 LRU 缓存importjava.util.LinkedHashMap;importjava.util.Map;publicclassLRUCacheK,VextendsLinkedHashMapK,V{privatefinalintcapacity;publicLRUCache(intcapacity){// 初始容量, 负载因子0.75, accessOrdertrue开启访问顺序模式super(capacity,0.75f,true);this.capacitycapacity;}OverrideprotectedbooleanremoveEldestEntry(Map.EntryK,Veldest){// 当当前大小超过指定容量时移除最老的条目链表头部returnsize()capacity;}}总结LinkedHashMap 实现 LRU 的本质是利用 HashMap 保证 get/put 的查找效率为 O(1)。利用 双向链表 维护访问顺序通过 accessOrdertrue 确保每次访问都将节点移至尾部。利用 removeEldestEntry 回调机制在插入新数据时自动检查并移除链表头部的“最老”数据从而严格控制缓存容量。这种实现方式比手动维护 HashMap 双向链表 更加简洁、安全且由 JDK 底层优化是 Java 工程中实现 LRU 缓存的首选方案。