Redis `maxmemory-policy`:淘汰策略 (LRU, LFU, Random) 的选择与影响

好的,没问题,直接进入主题!

各位朋友们,大家好!今天咱们来聊聊 Redis 里的一个非常重要的配置项:maxmemory-policy,也就是咱们常说的“淘汰策略”。这玩意儿直接关系到你的 Redis 数据库会不会被撑爆,以及撑爆之后该怎么优雅地释放空间。说白了,就是关乎你的数据会不会被“无情清退”。

想象一下,你开了一家超级受欢迎的咖啡馆,但是店面太小,只能容纳有限的顾客。当顾客超过容量时,你就必须决定让谁离开,好让新来的顾客能进来。maxmemory-policy 在 Redis 里扮演的就是这个“咖啡馆老板”的角色。

什么是 maxmemory-policy

简单来说,maxmemory-policy 就是 Redis 告诉你的,当内存使用达到 maxmemory 限制时,应该如何选择哪些键(key)被淘汰掉。maxmemory 是你设置的 Redis 可以使用的最大内存量。

默认情况下,如果 maxmemory 设置为 0 (也就是没有限制),那么 Redis 就不会主动删除任何键。但这往往不是我们想要的,因为内存总是有限的。

为什么要设置 maxmemory-policy

如果不设置,或者设置不合理,你的 Redis 实例可能会出现以下问题:

  • OOM (Out Of Memory) 错误: 当 Redis 无法分配更多内存时,会直接崩溃,导致服务中断。
  • 写入失败: 当 Redis 达到内存限制时,所有写入操作都会失败,数据无法更新。
  • 性能下降: 即使没有崩溃,当 Redis 频繁进行内存回收时,性能也会大幅下降。

maxmemory-policy 的可用选项

Redis 提供了多种不同的淘汰策略,每种策略都有其优缺点,适用于不同的场景。咱们一个个来看:

策略名称 描述 适用场景
noeviction 这是默认策略。当内存达到限制时,Redis 将拒绝所有新的写入操作。读操作仍然可以进行。 适用于对数据完整性要求极高,宁愿拒绝写入也不愿丢失数据的场景。
volatile-lru 从设置了过期时间的键中,移除最近最少使用的键 (Least Recently Used)。 适用于大部分数据都设置了过期时间,并且希望优先保留最近经常访问的数据的场景。例如,缓存系统。
allkeys-lru 从所有键中,移除最近最少使用的键。 适用于所有数据都同等重要,并且希望优先保留最近经常访问的数据的场景。
volatile-lfu 从设置了过期时间的键中,移除最近最不常用的键 (Least Frequently Used)。 适用于大部分数据都设置了过期时间,并且希望优先保留访问频率较高的数据的场景。LFU 相比 LRU,更关注键的访问频率,而不是最近一次的访问时间。
allkeys-lfu 从所有键中,移除最近最不常用的键。 适用于所有数据都同等重要,并且希望优先保留访问频率较高的数据的场景。
volatile-random 从设置了过期时间的键中,随机移除一个键。 适用于对数据价值没有明确判断标准,或者希望简单快速地释放空间的场景。
allkeys-random 从所有键中,随机移除一个键。 适用于对数据价值没有明确判断标准,或者希望简单快速地释放空间的场景。
volatile-ttl 从设置了过期时间的键中,移除剩余生存时间 (TTL) 最短的键。 适用于希望优先删除即将过期的数据的场景。例如,缓存系统,可以优先清理掉已经快要失效的缓存数据。

深入理解各种策略

咱们来更深入地了解一下这些策略,以及它们的一些细微差别。

  • LRU (Least Recently Used)

    LRU 策略认为,最近被使用过的数据,将来被再次使用的可能性越大。因此,它会优先淘汰最近最少使用的数据。

    举个例子: 假设你的 Redis 里存储了用户 session 数据。如果用户最近登录过,那么他的 session 数据就会被经常访问,也就不会被 LRU 淘汰。相反,如果用户很久没登录了,他的 session 数据就会被 LRU 认为是不重要的,从而被淘汰掉。

    LRU 的实现原理: Redis 使用近似 LRU 算法。它并不会维护一个全局的 LRU 列表,而是随机抽取几个键,然后比较它们的访问时间戳,淘汰掉最老的那个。 这种近似的方式可以大大降低算法的复杂度,提高性能。

    代码示例 (模拟 LRU 缓存):

    class LRUCache:
        def __init__(self, capacity):
            self.capacity = capacity
            self.cache = {}
            self.access_history = [] # 记录访问顺序
    
        def get(self, key):
            if key in self.cache:
                # 更新访问顺序
                self.access_history.remove(key)
                self.access_history.append(key)
                return self.cache[key]
            else:
                return None
    
        def put(self, key, value):
            if key in self.cache:
                # 更新值和访问顺序
                self.cache[key] = value
                self.access_history.remove(key)
                self.access_history.append(key)
            else:
                if len(self.cache) >= self.capacity:
                    # 淘汰最老的键
                    oldest_key = self.access_history.pop(0)
                    del self.cache[oldest_key]
    
                self.cache[key] = value
                self.access_history.append(key)
    
    # 使用示例
    cache = LRUCache(3)
    cache.put("A", 1)
    cache.put("B", 2)
    cache.put("C", 3)
    print(cache.get("A")) # 输出 1,  A 变成最近访问的
    cache.put("D", 4) #  淘汰 B (最老的)
    print(cache.get("B")) # 输出 None (已经被淘汰)
  • LFU (Least Frequently Used)

    LFU 策略认为,访问频率越低的数据,将来被再次使用的可能性越小。因此,它会优先淘汰访问频率最低的数据。

    举个例子: 假设你的 Redis 里存储了新闻文章。如果某篇文章发布后被很多人阅读,那么它的访问频率就会很高,也就不会被 LFU 淘汰。相反,如果某篇文章发布后很少有人阅读,那么它的访问频率就会很低,从而被 LFU 淘汰掉。

    LFU 的实现原理: Redis 使用近似 LFU 算法。它会为每个键维护一个计数器,记录该键的访问频率。当需要淘汰数据时,会随机抽取几个键,然后比较它们的计数器,淘汰掉计数器最小的那个。

    代码示例 (模拟 LFU 缓存):

    class LFUCache:
        def __init__(self, capacity):
            self.capacity = capacity
            self.cache = {}
            self.frequencies = {} # 记录访问频率
    
        def get(self, key):
            if key in self.cache:
                # 增加访问频率
                self.frequencies[key] += 1
                return self.cache[key]
            else:
                return None
    
        def put(self, key, value):
            if key in self.cache:
                # 更新值和访问频率
                self.cache[key] = value
                self.frequencies[key] += 1
            else:
                if len(self.cache) >= self.capacity:
                    # 淘汰访问频率最低的键
                    min_freq_key = min(self.frequencies, key=self.frequencies.get)
                    del self.cache[min_freq_key]
                    del self.frequencies[min_freq_key]
    
                self.cache[key] = value
                self.frequencies[key] = 1
    
    # 使用示例
    cache = LFUCache(3)
    cache.put("A", 1)
    cache.put("B", 2)
    cache.put("C", 3)
    cache.get("A") # 访问 A
    cache.get("A") # 再次访问 A
    cache.put("D", 4) # 淘汰 B (因为 B 的访问频率最低)
    print(cache.get("B")) # 输出 None (已经被淘汰)
    print(cache.get("A")) # 输出 1

    LRU vs LFU:

    • LRU 更关注最近的访问行为,而 LFU 更关注整体的访问频率。
    • LRU 对突发流量更敏感,可能会错误地淘汰掉一些长期不访问但仍然重要的数据。
    • LFU 可以更好地应对长期存在的热点数据,但对新加入的数据不够友好。
  • Random

    Random 策略就比较简单粗暴了,它会随机地选择一个键进行淘汰。 这种策略适用于对数据价值没有明确判断标准,或者希望简单快速地释放空间的场景。

    代码示例 (模拟 Random 淘汰):

    import random
    
    class RandomCache:
        def __init__(self, capacity):
            self.capacity = capacity
            self.cache = {}
    
        def get(self, key):
            if key in self.cache:
                return self.cache[key]
            else:
                return None
    
        def put(self, key, value):
            if key in self.cache:
                self.cache[key] = value
            else:
                if len(self.cache) >= self.capacity:
                    # 随机淘汰一个键
                    random_key = random.choice(list(self.cache.keys()))
                    del self.cache[random_key]
    
                self.cache[key] = value
    
    # 使用示例
    cache = RandomCache(3)
    cache.put("A", 1)
    cache.put("B", 2)
    cache.put("C", 3)
    cache.put("D", 4) # 随机淘汰 A, B, 或 C 中的一个
  • TTL (Time To Live)

    TTL 策略会优先淘汰剩余生存时间 (TTL) 最短的键。 这种策略适用于希望优先删除即将过期的数据的场景。

    代码示例 (模拟 TTL 淘汰):

    import time
    
    class TTL:
        def __init__(self, capacity):
            self.capacity = capacity
            self.cache = {}
            self.expire_times = {} # 记录过期时间
    
        def get(self, key):
            if key in self.cache:
                if self.expire_times[key] > time.time():
                    return self.cache[key]
                else:
                    # 已经过期,删除
                    del self.cache[key]
                    del self.expire_times[key]
                    return None
            else:
                return None
    
        def put(self, key, value, ttl):
            if key in self.cache:
                self.cache[key] = value
                self.expire_times[key] = time.time() + ttl
            else:
                if len(self.cache) >= self.capacity:
                    # 淘汰 TTL 最短的键
                    min_ttl_key = min(self.expire_times, key=self.expire_times.get)
                    del self.cache[min_ttl_key]
                    del self.expire_times[min_ttl_key]
    
                self.cache[key] = value
                self.expire_times[key] = time.time() + ttl
    
    # 使用示例
    cache = TTL(3)
    cache.put("A", 1, 5) # 5秒后过期
    cache.put("B", 2, 10) # 10秒后过期
    cache.put("C", 3, 2) # 2秒后过期
    time.sleep(3) # 等待 3 秒
    print(cache.get("C")) # 输出 None (C 已经过期)

如何选择合适的 maxmemory-policy

选择合适的 maxmemory-policy 需要根据你的应用场景和数据特点来决定。以下是一些建议:

  1. 如果你的数据都是缓存,并且设置了过期时间: 建议使用 volatile-lru, volatile-lfu, 或者 volatile-ttl
  2. 如果你的数据都是缓存,但是没有设置过期时间: 建议使用 allkeys-lruallkeys-lfu,但是需要注意,这可能会导致重要的数据被意外淘汰。
  3. 如果你的数据既有缓存,也有持久化数据: 建议只对缓存数据设置过期时间,然后使用 volatile-lru, volatile-lfu, 或者 volatile-ttl。 这样可以保证持久化数据不会被淘汰。
  4. 如果你对数据的重要性没有明确判断标准: 可以考虑使用 volatile-randomallkeys-random
  5. 如果你对数据完整性要求极高,宁愿拒绝写入也不愿丢失数据: 可以使用 noeviction,但是需要确保你的应用程序能够处理写入失败的情况。

配置 maxmemorymaxmemory-policy

你可以在 Redis 的配置文件 redis.conf 中设置 maxmemorymaxmemory-policy

maxmemory 1024mb # 设置最大内存为 1GB
maxmemory-policy allkeys-lru # 设置淘汰策略为 allkeys-lru

也可以通过 CONFIG SET 命令动态地修改这些配置:

redis-cli config set maxmemory 1024mb
redis-cli config set maxmemory-policy allkeys-lru

需要注意的是,动态修改配置可能会导致 Redis 实例重启,从而影响服务可用性。

maxmemory-samples 参数

Redis 的 LRU/LFU 算法是近似的,而不是精确的。 maxmemory-samples 参数控制了每次淘汰数据时,Redis 随机抽取的键的数量。 默认值为 5。

增加 maxmemory-samples 可以提高淘汰算法的精度,但也会增加 CPU 的开销。

maxmemory-samples 5 # 默认值

总结

maxmemory-policy 是 Redis 中一个非常重要的配置项,它决定了当 Redis 达到内存限制时,应该如何淘汰数据。 选择合适的策略需要根据你的应用场景和数据特点来决定。 希望今天的讲解能帮助你更好地理解 Redis 的淘汰策略,并为你的应用选择最合适的配置。

记住,没有万能的策略,只有最适合你的策略!

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注