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实现最简单方法
实现 LRU(Least Recently Used,最近最少使用)缓存最简单的方法取决于你的应用场景:是工程落地还是面试/算法考察。
- 工程落地最简单:继承 LinkedHashMap (Java)
在 Java 实际开发中,无需手写链表,直接利用 JDK 自带的 LinkedHashMap 即可快速实现一个线程非安全的 LRU 缓存。这是最简洁、最高效的工程实现方式。
核心原理:
LinkedHashMap 内部维护了一个双向链表来记录插入顺序或访问顺序。通过构造函数开启“访问顺序模式”,并重写 removeEldestEntry 方法,即可自动淘汰最久未使用的数据。
代码示例:
importjava.util.LinkedHashMap;importjava.util.Map;publicclassSimpleLRUCache<K,V>extendsLinkedHashMap<K,V>{privatefinalintcapacity;publicSimpleLRUCache(intcapacity){// 第三个参数 true 表示按照访问顺序排序,false 表示按照插入顺序排序super(capacity,0.75f,true);this.capacity=capacity;}@OverrideprotectedbooleanremoveEldestEntry(Map.Entry<K,V>eldest){// 当地图大小超过指定容量时,移除最老的条目returnsize()>capacity;}}简单测试一下
publicclassLRUTest{publicstaticvoidmain(String[]args){// 创建容量为 3 的 LRU 缓存LRUCache<Integer,String>cache=newLRUCache<>(3);// 1. 插入数据cache.put(1,"A");cache.put(2,"B");cache.put(3,"C");System.out.println("初始状态: "+cache);// 输出: {1=A, 2=B, 3=C}// 2. 访问 Key 1,将其移至链表尾部(变为最近使用)cache.get(1);System.out.println("访问1后: "+cache);// 输出: {2=B, 3=C, 1=A}// 3. 插入新数据 Key 4,此时容量已满,应淘汰最久未使用的 Key 2cache.put(4,"D");System.out.println("插入4后: "+cache);// 输出: {3=C, 1=A, 4=D}// 验证: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.capacity=capacity;this.cache=newMap();}get(key){if(!this.cache.has(key))return-1;// 关键步骤:先删除再重新插入,将其移到 Map 末尾(代表最近使用)constvalue=this.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.size>=this.capacity){// 如果满了,删除 Map 的第一个元素(最久未使用)constfirstKey=this.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 实现 LRU(Least 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 构造函数中有一个关键参数 accessOrder:
false(默认):插入顺序。元素按照放入 Map 的顺序排列,新元素加到链表尾部。这种模式不体现 LRU 特性。
true:访问顺序。这是实现 LRU 的关键。每当调用 get() 或 put()(更新已存在的 key)时,被操作的节点会被移动到双向链表的尾部(即最近使用端)。
3. LRU 核心逻辑实现步骤
A. 访问时移动节点(保持新鲜度)
当 accessOrder 为 true 时:
Get 操作:通过 HashMap 快速找到节点后,调用内部方法将该节点从当前位置移除,并重新链接到双向链表的尾部。
Put 操作:如果 Key 已存在,更新 Value 后,同样将该节点移动到链表尾部。
这一过程确保了:链表头部始终是最久未被访问的数据,链表尾部始终是最近被访问的数据。
B. 自动淘汰机制(移除最旧数据)
为了实现缓存容量限制,LinkedHashMap 提供了一个受保护的方法 removeEldestEntry(Map.Entry<K,V> eldest)。
默认行为:该方法默认返回 false,即不删除任何元素。
LRU 实现技巧:用户只需继承 LinkedHashMap 并重写该方法,当 size() > capacity 时返回 true。
触发时机:每次执行 put 操作添加新元素后,LinkedHashMap 会自动调用 removeEldestEntry。如果返回 true,它会自动移除双向链表头部(header.after)的节点,因为那里存放的就是最久未使用的数据。
4. 代码示例
通过极简的代码即可实现一个标准的 LRU 缓存:
importjava.util.LinkedHashMap;importjava.util.Map;publicclassLRUCache<K,V>extendsLinkedHashMap<K,V>{privatefinalintcapacity;publicLRUCache(intcapacity){// 初始容量, 负载因子0.75, accessOrder=true开启访问顺序模式super(capacity,0.75f,true);this.capacity=capacity;}@OverrideprotectedbooleanremoveEldestEntry(Map.Entry<K,V>eldest){// 当当前大小超过指定容量时,移除最老的条目(链表头部)returnsize()>capacity;}}- 总结
LinkedHashMap 实现 LRU 的本质是:
利用 HashMap 保证 get/put 的查找效率为 O(1)。
利用 双向链表 维护访问顺序,通过 accessOrder=true 确保每次访问都将节点移至尾部。
利用 removeEldestEntry 回调机制,在插入新数据时自动检查并移除链表头部的“最老”数据,从而严格控制缓存容量。
这种实现方式比手动维护 HashMap + 双向链表 更加简洁、安全,且由 JDK 底层优化,是 Java 工程中实现 LRU 缓存的首选方案。