Redis内存淘汰策略LRU与LFU到底怎么选?

来源:网络编程作者:越南程序员头衔:程序员
导读:本期聚焦于越南程序员创作的《Redis内存淘汰策略LRU与LFU到底怎么选?》,敬请观看详情。Redis 的内存淘汰策略并不完全等同于教科书里的 LRU 与 LFU 算法,而是在近似采样和频率衰减上做了工程化改造。LRU 依赖最近访问时间,通过随机采样若干键并淘汰最久未访问的一个,适合读多写少、热点相对集中的缓存;LFU 在 LRU 基础上引入访问频次与时间衰减,能够识别并保留长期高频键,但对新键的冷启动不够友好。理解这两者的差别,需要先搞清楚 Redis 对象头里的访问时间戳和衰减计数如何维护,再结合 maxmemory-policy 参数选择 allkeys-lru、volatile-lfu 等不同策略。本文从内部结构、近似算法、参数调优和典型误用四个层面展开,帮助你在内存达到上限时做出更稳妥的淘汰决策。

当 Redis 的可用内存达到 maxmemory 上限时,是否继续接受写入、淘汰哪些键,完全由 maxmemory-policy 决定。很多初学者只知道 LRU 代表最近最少使用、LFU 代表最不经常使用,但把标准算法直接套到 Redis 上就会出现偏差,因为 Redis 为了实现高吞吐和低内存开销,对两者都做了近似实现。

Redis内存淘汰策略LRU与LFU到底怎么选?

先明确一个前提:Redis 的淘汰策略并不是精确算法。标准 LRU 通常需要双向链表加哈希表来维护严格的访问顺序,标准 LFU 也要为每个键保存频次并构建高效排序结构。如果 Redis 为每个键都维护这些额外信息,内存占用和每次访问的写放大都会明显上升。尤其 Redis 是单线程处理命令,任何高开销操作都会直接拉高延迟。因此它选择在 redisObject 结构体里用一个 24 位的 lru 字段来承载淘汰信息,并通过随机采样降低单次决策成本。

一、先理解 Redis 内存淘汰的整体框架

Redis 触发淘汰的唯一前提是 maxmemory 大于 0 并且当前使用内存达到该阈值。若 maxmemory 设置为 0,在 64 位系统上等同于不设上限,但内存耗尽时仍会被操作系统终止,所以生产环境一般都会显式配置一个上限。当达到上限后,Redis 会按照 maxmemory-policy 执行动作,其中 noeviction 是默认策略,表示直接拒绝可能新增内存的写命令,但读命令和删除命令不受影响。

策略名称带有 volatile 的只能淘汰设置了过期时间的键,带有 allkeys 的则面向所有键。这是一个经常被忽略的区别。如果你的业务缓存键大多数没有设置 TTL,使用 volatile-lru 会因为没有可选淘汰对象而导致写入失败。真正通用的缓存场景应当优先考虑 allkeys-lru 或 allkeys-lfu。

Redis 的淘汰发生在每次执行写命令之前。它先检查内存是否超限,如果超限则从目标键空间中逐出一些键,直到内存回落到阈值以下或没有可淘汰的键。为了不让单个命令卡住太久,逐出过程有循环次数限制,这也是一些大键场景下内存仍然短暂超过 maxmemory 的原因。

二、LRU:近似采样如何逼近真实结果

标准 LRU 的核心是每次访问都把键移动到队头,当容量不足时淘汰队尾键。这种方案在内存对象数量很大时,链表节点和指针会带来不小的额外开销,而且每次访问都要修改链表,写入量较大。Redis 的近似 LRU 随机取若干个键,比较它们最后一次访问时间,淘汰最久未访问的键。

redisObject.lru 在 LRU 策略下保存的是该键最后一次被访问的时间戳,单位是秒。每次读取或写入键时,Redis 都会把当前 UNIX 时间更新到这个字段。淘汰时计算 当前时间 - lru 得到空闲时间,空闲时间最大的键被淘汰。默认采样数量由 maxmemory-samples 控制,值为 5。这个默认值在大多数场景下已经比较接近精确 LRU,但如果键数量非常多且访问模式复杂,可以提高到 10。

Redis 的近似 LRU 还引入了淘汰候选池机制,避免每次随机采样的结果抖动过大。候选池会保留若干空闲时间较长的键,新采样到的键会和候选池中的键共同排序,最终选择空闲时间最大的那个淘汰。以下是一段简化后的采样与候选池填充逻辑:

/* Redis 近似 LRU 淘汰:随机采样并选择空闲时间最大的键 */
int evictionPoolPopulate(dict *sampleDict, dict *keyDict, evictionPoolEntry *pool) {
    int count = 0;
    while (count < EVPOOL_SIZE) {
        dictEntry *de = dictGetRandomKey(sampleDict);
        if (de == NULL) break;
        unsigned long long idle = estimateObjectIdleTime(dictGetVal(de));
        /* 根据空闲时间插入候选池,空闲时间越大排名越靠前 */
        insertIntoEvictionPool(pool, de, idle);
        count++;
    }
    return count;
}

可以看到,Redis 不会扫描全部键,也不会维护一条全局 LRU 链表。这种设计牺牲了一点精确性,却换来了更少的内存元数据和更短的执行时间。实际测试中,采样数设为 10 时,近似 LRU 与标准 LRU 的行为重合度已经很高,而开销仍远低于标准实现。

使用 LRU 策略时还需要注意,短时间内批量访问大量冷数据会污染缓存。比如一个定时任务扫过几百万个键,这些键的访问时间被刷新,原本热点的键反而可能因为空闲时间变长而被淘汰。这是 LRU 本身对偶发批量访问比较敏感的表现,也是后来引入 LFU 的一个重要原因。

三、LFU:频次统计与时间衰减的落地方式

LFU 解决的核心问题是:一个键被访问过一次,不代表它以后会被频繁访问。LRU 只看最近时间,无法区分高频热点和偶尔访问的键。标准 LFU 为每个键维护频次计数器,淘汰频次最低的键。但 Redis 不能简单地对每次访问加一,因为长时间运行的旧键会积累极高的频次,新键永远竞争不过旧键。

Redis 在 lru 字段中同时保存了两个信息:高 16 位存最后访问时间,以分钟为单位;低 8 位存频次计数器,范围是 0 到 255。访问键时,频次不会无条件加一,而是按照概率递增。概率函数大致为 1/(counter * lfu_log_factor + 1),lfu_log_factor 默认是 10。计数器越大,增长越慢,这样频次不会无限膨胀。

只增加频次还不够,因为一旦某个键不再被访问,它仍然可能长期保持高频次,占用缓存。于是 Redis 引入了时间衰减机制,由 lfu-decay-time 控制,单位是分钟。默认值为 1,表示键每空闲一分钟,频次就会按一定规则降低一次。这样长期不活跃的旧热点会逐渐让位给新热点。以下是一段简化后的 LFU 频次更新逻辑:

/* Redis 近似 LFU:访问键时按概率增长频次,并随时间衰减 */
void updateLFU(robj *val) {
    unsigned long counter = LFUDecrAndReturn(val);
    counter = LFULogIncr(counter);
    val->lru = (LFUGetTimeInMinutes() << 8) | counter;
}

unsigned long LFULogIncr(unsigned long counter) {
    if (counter == 255) return counter;
    double r = (double)rand() / RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    double p = 1.0 / (baseval * server.lfu_log_factor + 1);
    if (r < p) counter++;
    return counter;
}

LFU 的缺点是冷启动问题。新写入的键初始频次很低,在内存紧张时可能很快被淘汰,尤其是缓存刚启动或大批量导入数据时。可以通过调小 lfu_log_factor 让频次增长更快,或者使用 volatile-lfu 只淘汰设置了过期时间的键,以保护不过期的元数据类键。

另一个容易误解的地方是,LFU 并不适合所有场景。如果业务访问本身就非常均匀,没有明显高频键,LFU 的频次区分度会很弱,维护频次和衰减反而增加 CPU 开销。此时 LRU 可能更简单可靠。

四、LRU 与 LFU 的对比及调优建议

下面从几个维度对比两种策略:

维度LRULFU
淘汰依据最近访问时间访问频次
核心优势实现简单,对短期热点响应快能保留长期高频键,抗偶发批量访问
主要劣势批量冷数据访问会污染缓存新键冷启动不友好,计算略复杂
适用场景读多写少、热点相对集中长期稳定热点、冷数据多但偶发访问

如果你不确定该用哪个,可以从 allkeys-lru 开始。这是 Redis 社区多年验证过的通用选择,命中率通常比较稳定。当你观察到某些高频键频繁被换出,或者定期批量任务导致命中率周期性下降时,再切换到 allkeys-lfu 并对比监控数据。切换策略不需要重启,直接修改配置文件或者运行时发送 CONFIG SET maxmemory-policy allkeys-lfu 即可。

调优参数时不要只看命中率这一项,还要关注 Redis 单线程延迟。例如 maxmemory-samples 从 5 提高到 20,确实能让 LRU 更接近精确淘汰,但每次淘汰都要比较更多样本,CPU 消耗会上升。对于 LFU,lfu-log-factor 越大,频次增长越慢,更适合长期运行的缓存;lfu-decay-time 越大,历史频次保留越久,热点切换越慢。

最后要注意,淘汰策略只能缓解内存压力,不能替代容量规划。如果业务数据量长期超过可用内存,无论如何调优都会出现频繁淘汰和命中率下跌。正确做法是根据业务增长预留内存空间,配合过期时间、持久化策略和集群扩容,让淘汰策略只作为突发情况的缓冲,而不是长期超载的遮羞布。

Redis内存淘汰LRU算法LFU算法修改时间:2026-10-01 20:02:44

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1001/64375.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。