过期策略
Redis 的 过期策略(Expiration Policy)决定了 如何管理和删除已过期的 key,确保内存资源的合理使用。Redis 提供了 三种过期策略:
1. 惰性删除(Lazy Deletion)
特点:只有当客户端访问某个 key 时,Redis 才会检查它是否过期,过期则删除,否则继续保留。
工作方式
- 当客户端执行
GET
、SET
、EXISTS
等操作访问某个 key 时,Redis 会检查该 key 是否过期。 - 如果过期,则立即删除,并返回
nil
(对于GET
操作)。 - 如果未过期,则正常返回 key 的值。
优点
✅ 访问少的过期 key 不会占用 CPU 资源进行扫描,减少 Redis 负担。
✅ 适用于对响应时间敏感的业务,因为删除操作分散到用户访问时进行。
缺点
❌ 过期 key 可能长时间占用内存,如果某些 key 过期但长期未访问,它们不会被删除,导致内存泄漏。
2. 定期删除(Periodic Deletion)
特点:Redis 每隔一段时间主动扫描部分 key,删除已过期的 key,以减少内存占用。
工作方式
- Redis 每 100 毫秒(默认)会随机抽取若干个设置了过期时间的 key 进行检查,并删除已过期的 key。
- 但不会遍历所有 key,而是抽样检查,防止影响 Redis 性能。
优点
✅ 能够主动释放部分过期 key,避免内存无限增长。
✅ 相比惰性删除,更能减少过期 key 造成的内存泄漏。
缺点
❌ 无法确保所有过期 key 都被及时删除,部分 key 仍可能滞留在内存。
❌ 如果 key 过多,可能会有一定的过期 key 滞留,导致内存占用高于预期。
3. 内存淘汰(Eviction)
特点:当 Redis 内存达到
maxmemory
上限时,触发内存淘汰策略,主动删除部分 key 以腾出空间。
工作方式
- 当内存不足时,Redis 先尝试删除已过期的 key 。
- 如果过期 key 不够,Redis 按照
maxmemory-policy
设定的策略(如 LRU、LFU、随机删除等)进行 key 淘汰。 - 这部分策略与 Redis 内存淘汰策略(Eviction Policy) 相关。
优点
✅ 确保 Redis 不会因过多过期 key 占用内存而崩溃。
✅ 配合 LRU/LFU 策略,可以更智能地淘汰低价值数据。
缺点
❌ 只有当内存达到上限时才会触发,不适用于低内存占用场景。
❌ 淘汰策略可能会删除未过期但仍然有用的数据,影响业务逻辑。
Redis 过期策略对比
过期策略 | 触发时机 | 作用 | 优缺点 |
---|---|---|---|
惰性删除 | 访问 key 时 | 仅在 key 被访问时检查并删除 | 节省 CPU 资源,但可能导致内存泄漏 |
定期删除 | Redis 定时任务(默认 100ms 一次) | 随机扫描部分 key 并删除过期 key | 减少内存占用,但可能无法完全清理过期数据 |
内存淘汰 | 内存达到 maxmemory 限制 | 触发 maxmemory-policy 策略删除数据 | 避免内存溢出,但可能删除未过期的重要数据 |
如何查看 Redis 过期 key 的统计信息?
Redis 提供了 INFO
命令,可以查看过期 key 相关的统计信息:
redis-cli INFO stats | grep expired
示例返回:
expired_keys:12345 # 说明有 12345 个 key 过期并被删除
expired_stale_perc:10.56 # 说明大约 10.56% 的 key 超过 TTL 仍未被删除
如何优化 Redis 过期 key 处理?
1️⃣ 尽量使用合适的 maxmemory-policy
:
- 缓存业务(如热点数据):建议
allkeys-lru
,删除最近最少使用的 key。 - 严格控制过期 key:建议
volatile-ttl
,优先删除即将过期的数据。
2️⃣ 手动清理过期 key:
如果系统发现过期 key 过多,可以主动执行:
redis-cli keys "*" | xargs redis-cli del
⚠ 警告:keys "*"
在大数据量时可能会导致 Redis 停滞,应谨慎使用。
3️⃣ 使用 SCAN
替代 KEYS
:
redis-cli --scan --pattern "prefix:*" | xargs redis-cli del
SCAN
命令不会阻塞 Redis,可以安全地遍历 key 并删除过期数据。
总结
- Redis 采用 惰性删除 + 定期删除 + 内存淘汰 的组合策略来管理过期 key。
- 惰性删除:访问 key 时才检查是否过期,节省 CPU 但可能导致内存泄漏。
- 定期删除:Redis 定期随机检查并删除部分过期 key,减少过期 key 堆积问题。
- 内存淘汰:当内存不足时触发,配合
maxmemory-policy
策略删除 key。 - 优化方案:
- 短期热点数据(如缓存):推荐
allkeys-lru
。 - 严格控制 key 过期:推荐
volatile-ttl
。 - 手动定期清理过期数据(如 SCAN + DEL)。
- 短期热点数据(如缓存):推荐
如果你的业务对内存敏感,建议结合 maxmemory-policy
进行优化,以确保 Redis 在高并发下依然能够稳定运行。 🚀
内存淘汰策略
Redis 的内存淘汰策略(Eviction Policy)主要用于在内存达到上限(maxmemory
配置的限制)时决定如何删除数据,以便腾出空间存储新数据。Redis 提供了多种淘汰策略,可通过 maxmemory-policy
参数进行配置。常见策略如下:
1. volatile-lru(Least Recently Used,最近最少使用)
- 只对设置了过期时间(TTL)的 key 进行 LRU 淘汰。
- 当内存不足时,优先淘汰最近最少使用(LRU)的 key。
2. allkeys-lru(淘汰所有 key,LRU)
- 所有 key(无论是否有过期时间)都可以参与淘汰。
- 当内存不足时,Redis 根据 LRU 算法淘汰最近最少使用的 key。
3. volatile-lfu(Least Frequently Used,最近最少使用频率)
- 仅针对设置了过期时间的 key 进行 LFU 淘汰。
- 淘汰访问频率(访问次数最少)的 key。
4. allkeys-lfu(淘汰所有 key,LFU)
- 所有 key(无论是否有过期时间)都可以参与淘汰。
- Redis 根据 LFU 算法淘汰访问次数最少的 key。
5. volatile-random(随机淘汰)
- 仅针对设置了过期时间的 key 进行随机淘汰。
6. allkeys-random(所有 key 随机淘汰)
- 所有 key 都可以被随机淘汰,而不考虑 LRU 或 LFU。
7. volatile-ttl(根据 TTL 淘汰)
- 仅针对设置了过期时间的 key 进行淘汰。
- 优先淘汰TTL 最短的 key(即最早过期的 key)。
8. noeviction(拒绝新写入)
- 不淘汰任何 key,而是直接返回错误(如
OOM command not allowed
)。 - 适用于严格控制数据不被删除的场景,比如缓存预热或全量数据存储。
如何选择合适的淘汰策略?
策略 | 适用场景 |
---|---|
volatile-lru | 适用于缓存,只淘汰可过期的 key,保留重要数据。 |
allkeys-lru | 适用于高性能缓存,确保经常访问的数据优先保留。 |
volatile-lfu | 适用于访问频率不均衡的缓存,淘汰低频访问的过期 key。 |
allkeys-lfu | 适用于热点数据,确保最常访问的数据留存。 |
volatile-random | 适用于非热点数据缓存,随机清理部分过期数据。 |
allkeys-random | 适用于数据无访问优先级的场景,如消息队列。 |
volatile-ttl | 适用于短时缓存,如临时验证码存储。 |
noeviction | 适用于严格限制数据丢失的场景,如用户 session 数据。 |
如何查看 Redis 当前的淘汰策略?
使用 Redis 命令:
redis-cli config get maxmemory-policy
如何修改 Redis 淘汰策略?
- 临时修改(重启后失效):
redis-cli config set maxmemory-policy allkeys-lru
- 永久修改(写入
redis.conf
配置文件):maxmemory-policy allkeys-lru
LRU 与 LFU 的区别
算法 | 计算方式 | 适用场景 |
---|---|---|
LRU(Least Recently Used) | 淘汰最近最少使用的 key | 适用于时间相关的缓存,如热点新闻。 |
LFU(Least Frequently Used) | 淘汰访问次数最少的 key | 适用于长期热门数据,如 API 访问统计。 |
如果你的应用需要保留最常访问的数据,建议选择 LFU;如果需要保留最近使用的数据,建议选择 LRU。
总结
- Redis 支持 8 种内存淘汰策略,可根据业务场景选择合适的策略。
- LRU 适用于短期热点缓存,LFU 适用于长期热点缓存。
- volatile- 前缀的策略仅淘汰设置了过期时间的 key,而 allkeys- 策略适用于所有 key。
- noeviction 适用于不希望数据被自动删除的业务,如用户登录状态管理。
如果你的业务主要是缓存,推荐使用 allkeys-lru
或 volatile-lru
,这样能尽可能保留高价值的数据。
LRU 示例代码
下面是一个使用 Java 实现 LRU(Least Recently Used,最近最少使用)算法的示例代码:
这个实现使用了 LinkedHashMap
,它自带维护元素插入顺序或访问顺序的功能,非常适合用来实现 LRU 缓存。我们可以通过重写 removeEldestEntry
方法来限制缓存的大小并删除最不常用的元素。
import java.util.LinkedHashMap;
import java.util.Map;public class LRUCache<K, V> {private final int capacity;private final Map<K, V> cache;// 构造函数public LRUCache(int capacity) {this.capacity = capacity;// 使用 LinkedHashMap 以便维护插入顺序this.cache = new LinkedHashMap<>(capacity, 0.75f, true);}// 获取缓存中的值public V get(K key) {return cache.getOrDefault(key, null); // 返回值,若不存在则返回 null}// 将值插入缓存public void put(K key, V value) {// 如果缓存已满,移除最老的元素if (cache.size() >= capacity) {K eldestKey = cache.keySet().iterator().next();cache.remove(eldestKey);}cache.put(key, value); // 插入或更新缓存}// 打印缓存内容public void printCache() {System.out.println(cache);}// 测试 LRU 算法public static void main(String[] args) {LRUCache<Integer, String> lruCache = new LRUCache<>(3); // 容量为 3// 插入数据lruCache.put(1, "One");lruCache.put(2, "Two");lruCache.put(3, "Three");lruCache.printCache(); // 输出 {1=One, 2=Two, 3=Three}// 访问 key 2lruCache.get(2);lruCache.printCache(); // 输出 {1=One, 3=Three, 2=Two},key 2 最近访问,移动到最后// 插入新的数据,超出容量lruCache.put(4, "Four");lruCache.printCache(); // 输出 {3=Three, 2=Two, 4=Four},key 1 被淘汰// 插入新的数据,超出容量lruCache.put(5, "Five");lruCache.printCache(); // 输出 {2=Two, 4=Four, 5=Five},key 3 被淘汰}
}
代码解析:
- 构造函数:使用
LinkedHashMap
来存储缓存数据,true
参数保证了访问顺序,最近访问的元素会被移到末尾。 get
方法:获取缓存中的元素,如果元素不存在返回null
。put
方法:插入数据。如果缓存已满,使用LinkedHashMap
的keySet().iterator().next()
获取最老的元素并移除,确保缓存大小始终不超过指定的容量。printCache
方法:用于打印当前缓存内容,以便在测试中查看结果。
示例输出:
{1=One, 2=Two, 3=Three}
{1=One, 3=Three, 2=Two}
{3=Three, 2=Two, 4=Four}
{2=Two, 4=Four, 5=Five}
解释:
- 初始缓存包含了
1=One
,2=Two
,3=Three
。 - 访问
key 2
后,key 2
移动到缓存末尾,成为最近使用的项。 - 插入新的元素
4=Four
时,由于缓存容量为 3,key 1
被淘汰。 - 插入新的元素
5=Five
时,key 3
被淘汰,因为它是最久未访问的元素。
扩展:
如果你需要更复杂的 LRU 缓存逻辑(比如支持线程安全的操作),你可以通过使用 ConcurrentHashMap
和同步机制来实现。
LFU 示例代码
LFU(Least Frequently Used,最不常用)算法是根据数据的访问频率来管理缓存的,最不常访问的数据会被优先淘汰。实现 LFU 算法时,需要记录每个缓存项的访问次数,并根据访问频率来决定哪个元素最先被移除。
以下是用 Java 实现 LFU 缓存算法的示例代码:
import java.util.*;public class LFUCache<K, V> {// 缓存最大容量private final int capacity;// 存储缓存数据 (key -> value)private final Map<K, V> cache;// 存储缓存的访问次数 (key -> frequency)private final Map<K, Integer> frequencyMap;// 存储访问频率到 key 列表的映射 (frequency -> key list)private final Map<Integer, LinkedHashSet<K>> frequencyList;// 记录当前最小频率private int minFrequency;// 构造函数public LFUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>();this.frequencyMap = new HashMap<>();this.frequencyList = new HashMap<>();this.minFrequency = 0;}// 获取缓存的值public V get(K key) {if (!cache.containsKey(key)) {return null;}// 更新频率int frequency = frequencyMap.get(key);frequencyMap.put(key, frequency + 1);// 更新频率列表frequencyList.get(frequency).remove(key);if (frequencyList.get(frequency).isEmpty() && frequency == minFrequency) {minFrequency++;}frequencyList.computeIfAbsent(frequency + 1, k -> new LinkedHashSet<>()).add(key);return cache.get(key);}// 放入缓存public void put(K key, V value) {if (capacity <= 0) {return;}if (cache.containsKey(key)) {cache.put(key, value);get(key); // 更新频率return;}if (cache.size() >= capacity) {evict();}cache.put(key, value);frequencyMap.put(key, 1);frequencyList.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);minFrequency = 1;}// 删除最不常用的元素private void evict() {LinkedHashSet<K> leastFrequentKeys = frequencyList.get(minFrequency);K keyToEvict = leastFrequentKeys.iterator().next();leastFrequentKeys.remove(keyToEvict);if (leastFrequentKeys.isEmpty()) {frequencyList.remove(minFrequency);}cache.remove(keyToEvict);frequencyMap.remove(keyToEvict);}// 打印缓存内容public void printCache() {System.out.println(cache);}// 测试 LFU 算法public static void main(String[] args) {LFUCache<Integer, String> lfuCache = new LFUCache<>(3); // 容量为 3// 插入数据lfuCache.put(1, "One");lfuCache.put(2, "Two");lfuCache.put(3, "Three");lfuCache.printCache(); // 输出 {1=One, 2=Two, 3=Three}// 访问 key 1 和 key 2lfuCache.get(1);lfuCache.get(2);lfuCache.printCache(); // 输出 {1=One, 2=Two, 3=Three},key 1 和 key 2 的频率增加// 插入新的数据,超出容量lfuCache.put(4, "Four");lfuCache.printCache(); // 输出 {1=One, 2=Two, 4=Four},key 3 被淘汰,因为其访问频率最低// 插入新的数据,超出容量lfuCache.put(5, "Five");lfuCache.printCache(); // 输出 {2=Two, 4=Four, 5=Five},key 1 被淘汰,因为其访问频率最低}
}
代码解析:
cache
:存储缓存的实际数据,key -> value
映射。frequencyMap
:记录每个缓存项的访问频率,key -> frequency
映射。frequencyList
:使用一个LinkedHashSet
来存储具有相同访问频率的缓存项,frequency -> key list
映射。这样可以方便地找到哪些缓存项访问频率相同。minFrequency
:用于记录当前缓存中最小的访问频率,用来优化缓存淘汰时选择最少使用的元素。get
方法:返回缓存项的值并更新该项的访问频率。put
方法:插入新数据,若缓存已满,则调用evict()
方法淘汰最不常用的元素。evict
方法:淘汰最不常用的元素(即最少频率的元素),并从cache
和frequencyMap
中移除。printCache
方法:打印当前缓存的内容。
示例输出:
{1=One, 2=Two, 3=Three}
{1=One, 2=Two, 3=Three}
{1=One, 2=Two, 4=Four}
{2=Two, 4=Four, 5=Five}
解释:
- 初始化缓存容量为 3,插入
1=One
,2=Two
,3=Three
。 - 访问
key 1
和key 2
后,key 1
和key 2
的访问频率增加,key 3
的访问频率最低。 - 插入
key 4
时,key 3
被淘汰,因为它的访问频率最低。 - 插入
key 5
时,key 1
被淘汰,因为它的访问频率最低。
总结:
LFU 缓存算法根据元素的访问频率来进行缓存淘汰,最不常访问的元素最先被删除。这个实现通过 frequencyMap
和 frequencyList
有效地跟踪访问频率,并通过 evict
方法淘汰最不常用的缓存项。