当前位置: 首页 > article >正文

Redis 中的过期策略和内存淘汰策略

过期策略

Redis 的 过期策略(Expiration Policy)决定了 如何管理和删除已过期的 key,确保内存资源的合理使用。Redis 提供了 三种过期策略

1. 惰性删除(Lazy Deletion)

特点:只有当客户端访问某个 key 时,Redis 才会检查它是否过期,过期则删除,否则继续保留。

工作方式

  • 当客户端执行 GETSETEXISTS 等操作访问某个 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-lruLeast Recently Used,最近最少使用

  • 只对设置了过期时间(TTL)的 key 进行 LRU 淘汰。
  • 当内存不足时,优先淘汰最近最少使用(LRU)的 key。

2. allkeys-lru淘汰所有 key,LRU

  • 所有 key(无论是否有过期时间)都可以参与淘汰。
  • 当内存不足时,Redis 根据 LRU 算法淘汰最近最少使用的 key。

3. volatile-lfuLeast 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-lruvolatile-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 2
        lruCache.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 被淘汰
    }
}

代码解析

  1. 构造函数:使用 LinkedHashMap 来存储缓存数据,true 参数保证了访问顺序,最近访问的元素会被移到末尾。
  2. get 方法:获取缓存中的元素,如果元素不存在返回 null
  3. put 方法:插入数据。如果缓存已满,使用 LinkedHashMapkeySet().iterator().next() 获取最老的元素并移除,确保缓存大小始终不超过指定的容量。
  4. 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. 初始缓存包含了 1=One, 2=Two, 3=Three
  2. 访问 key 2 后,key 2 移动到缓存末尾,成为最近使用的项。
  3. 插入新的元素 4=Four 时,由于缓存容量为 3,key 1 被淘汰。
  4. 插入新的元素 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 2
        lfuCache.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 被淘汰,因为其访问频率最低
    }
}

代码解析

  1. cache:存储缓存的实际数据,key -> value 映射。
  2. frequencyMap:记录每个缓存项的访问频率,key -> frequency 映射。
  3. frequencyList:使用一个 LinkedHashSet 来存储具有相同访问频率的缓存项,frequency -> key list 映射。这样可以方便地找到哪些缓存项访问频率相同。
  4. minFrequency:用于记录当前缓存中最小的访问频率,用来优化缓存淘汰时选择最少使用的元素。
  5. get 方法:返回缓存项的值并更新该项的访问频率。
  6. put 方法:插入新数据,若缓存已满,则调用 evict() 方法淘汰最不常用的元素。
  7. evict 方法:淘汰最不常用的元素(即最少频率的元素),并从 cachefrequencyMap 中移除。
  8. 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}

解释

  1. 初始化缓存容量为 3,插入 1=One, 2=Two, 3=Three
  2. 访问 key 1key 2 后,key 1key 2 的访问频率增加,key 3 的访问频率最低。
  3. 插入 key 4 时,key 3 被淘汰,因为它的访问频率最低。
  4. 插入 key 5 时,key 1 被淘汰,因为它的访问频率最低。

总结

LFU 缓存算法根据元素的访问频率来进行缓存淘汰,最不常访问的元素最先被删除。这个实现通过 frequencyMapfrequencyList 有效地跟踪访问频率,并通过 evict 方法淘汰最不常用的缓存项。


http://www.kler.cn/a/613615.html

相关文章:

  • 项目-苍穹外卖(十六) Apache ECharts+数据统计
  • Vue学习笔记集--pnpm包管理器
  • 企业高效访问海外SAAS应用,SD-WAN出口网络专线提高办公效率
  • 蓝桥杯备考:DFS之数独
  • Unity高渲染管线
  • linux0.11内核源码修仙传第十一章——硬盘初始化
  • 数据库约束、常见语句等
  • VGG 改进:添加ScConv空间与通道特征重构卷积
  • pip show protobuf ValueError: invalid literal for int() with base 10: ‘‘
  • 【redis】前缀树 trie-radix tree-rax
  • 协作机械臂需要加安全墙吗? 安全墙 光栅 干涉区
  • 详细比较StringRedisTemplate和RedisTemplate的区别及使用方法,及解决融合使用方法
  • Go语言nil原理深度解析:底层实现与比较规则
  • ReAct: Synergizing Reasoning and Acting in Language Models
  • 数字化转型1061丨某著名企业新零售云业务中台总体解决方案(文末有下载方式)
  • ElasticSearch在Windows单节点部署及使用
  • 云原生周刊:Ingress-NGINX 漏洞
  • JDBC FetchSize不生效,批量变全量致OOM问题分析
  • 将网页操作的脚本自动保存成yaml ,然后修改使用
  • RK3568笔记八十一: Linux 小智AI聊天机器人移植