news 2026/10/19 20:11:07

Redis近似LRU淘汰机制详解:原理、调优与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis近似LRU淘汰机制详解:原理、调优与避坑指南

先别急着写代码调参,有个现象你大概率见过:明明 Redis 里还剩不少冷数据,可热 key 却被悄无声息地清理了,删完之后evicted_keys还在涨。很多人第一反应是“过期时间设错了”,其实是内存淘汰在起作用。这篇文章要聊的,就是 Redis 背后那套 LRU 近似实现——它怎么工作、为什么设计成现在这样、遇到淘汰不准时怎么排查,以及实际运维里最容易踩的坑。

LRU(Least Recently Used,最近最少使用)是缓存系统最经典也最常用的淘汰策略。但 Redis 并没有老老实实维护一份全局的“最久未使用”排序链表,而是用了一套“采样 + 近似时间戳”的轻量方案。这套设计从内存占用的角度非常划算,但代价是淘汰结果和教科书里的标准 LRU 并不完全一致。只要理解了这点,你再看到 Redis 删掉“看起来还热”的 key,就不会觉得它抽风了。


1. 为什么 Redis 不用真正的 LRU,而选了“近似 LRU”

1.1 标准 LRU 背后要付出的内存代价

先看一个纯数据结构层面的问题。要实现教科书级的 LRU,你通常得在哈希表之外再维护一个双向链表:每次访问一个 key,都要把它从链表当前位置摘下来,再移动到链表头部;内存超过上限时,直接从链表尾部删掉最久没用的那个节点。这套机制在容量可控的本地缓存里很常见,性能也稳定,但放在 Redis 的场景里就很不合适了。

Redis 一个实例里可能动辄上亿个 key,每个 key 都对应一个 redisObject 对象和若干个数据结构节点。如果每个 key 都额外带上两个链表指针(prev 和 next),内存开销会直接膨胀十几个字节起步。更麻烦的是,每次GET、SET都会触发一次链表节点移动,这在高并发场景下会产生大量锁竞争和指针操作。Redis 的定位是“极速访问”,为了一个淘汰算法就拖慢主路径,它肯定不干。

所以 Redis 走了另一条路:在每个对象的头部留出 24 bit 空间,只记录一个近似时间戳,然后用随机采样来模拟“最近最少使用”的判别。这种方案牺牲了一定的准确性,但单 key 的内存增量极小,而且访问主路径几乎不增加额外负担。对 Redis 这种追求性能和内存双优的中间件来说,这个取舍非常合理。

1.2 用“采样”代替“全局排序”

既然没有全局链表,Redis 怎么判断哪个 key 最久没被访问?答案是一次只比较一小撮。

当内存达到maxmemory阈值需要淘汰时,Redis 会从字典里随机抽出若干个 key,逐个计算它们上一次被访问距离现在有多久,挑出那个最“冷”的 key 删掉。整个比较范围不是全量 key,而是“随机样本”,所以准确度取决于样本大小。这个思路有很明确的统计基础:样本里的最大值虽然未必是全局最大值,但只要随机性够好,大概率能淘到一个足够老的 key,基本能覆盖淘汰需求。

这也就是“近似 LRU”名字的由来——不精确,但够用。Redis 官方也承认,在默认参数下,它的淘汰效果和标准 LRU 已经很接近,大部分业务场景完全可以接受。

1.3 落地到各个淘汰策略

近似 LRU 在 Redis 里最终体现为下面这些maxmemory-policy可选值:

策略作用范围行为说明
noeviction无不淘汰,内存满后写入直接报错
allkeys-lru全部 key对所有 key 按近似 LRU 淘汰
volatile-lru仅带 TTL 的 key只对设置了过期时间的 key 用近似 LRU 淘汰
allkeys-random全部 key随机淘汰
volatile-random仅带 TTL 的 key在带过期时间的 key 里随机淘汰
volatile-ttl仅带 TTL 的 key优先淘汰剩余 TTL 最短的 key
allkeys-lfu全部 keyRedis 4.0 之后可用,按访问频率+时间衰减淘汰
volatile-lfu仅带 TTL 的 key同 LFU,但只作用在带过期时间的 key 上

默认策略是noeviction。也就是说,如果不主动配置 LRU 相关策略,内存满了之后 Redis 不会删任何 key,而是直接对触发内存超限的写命令返回错误。很多刚接触 Redis 的人会在日志里看到OOM command not allowed when used memory > 'maxmemory',第一反应是服务器磁盘或进程挂了,其实只是没配淘汰策略。

选择volatile-lru还是allkeys-lru,取决于你对 key 生命周期的理解。如果绝大多数 key 缓存属性很强,都可以接受被淘汰,那就用allkeys-lru;如果有些 key 是业务依赖的核心数据,绝不能丢,只是它们恰好占了一部分内存,那就别用allkeys-*,改用volatile-lru,确保只有那些设置了 TTL 的缓存 key 会被淘汰。这个选择很重要,决定了“误删”的边界。


2. 一探内部:LRU 时钟、空闲时间计算与淘汰流程

2.1 24 bit 的 LRU 时钟到底能存多久

Redis 在redisObject结构体里用 24 bit 存 LRU 信息。这个字段在不同模式下语义不一样:LRU 模式下存的是访问时间戳,LFU 模式下会拆成高 16 位的计数器加低 8 位的衰减时间。这里只说 LRU。

24 bit 最大值是 16777215。Redis 早期版本用过毫秒级或者更低分辨率的时钟,导致很短时间LRU 时钟就回绕,淘汰判断经常失真。后来改成了秒级分辨率,也就是大约 194 天才会完整回绕一次。日常业务里,一个 Redis 实例连续运行几个月,达到 194 天时确实会遇到回绕情况,但 Redis 和直接减法的保护逻辑做了处理,可以正确处理回绕。

每个 key 被访问的时候,会把当时的全局 LRU 时钟复制到自己的对象头上。全局 LRU 时钟并不是每毫秒都变,而是由后台定时任务周期性刷新,大约 100ms 级别。所以你能观察到的“最近访问时间”本身就是粗粒度的,这一个细节就决定了 Redis 的 LRU 永远是近似的,不可能做到像素级精准。

2.2 空闲时间的计算与回绕处理

所谓“最久没访问”,在 Redis 里被量化成了“空闲时间”(idle time)。判断流程是这样的:拿到当前全局时钟now_lruclock,再看目标 key 对象里记录的key_lru,二者相减就是空闲秒数。

但如果当前时钟已经回绕过一轮,直接减会出现负数。所以 Redis 的判断逻辑分两种情况:

  • 如果now_lruclock >= key_lru,空闲时间就是now_lruclock - key_lru;
  • 如果now_lruclock < key_lru,说明时钟已经绕了一圈,空闲时间要用key_lru - now_lruclock取补,再换算成整体距离。

这个逻辑看起来不起眼,却是保障 194 天回绕后淘汰依然有效的前提。有些二次开发版本或者早期版本在这里处理得粗糙,运行一段时间后淘汰行为就会变得很怪。

2.3 一次淘汰动作完整拆解

下面模拟一个实际场景:实例设置了maxmemory 256mb、maxmemory-policy allkeys-lru,当前内存已经达到 268mb,此刻来了一条写命令。Redis 的处理大致是:

  1. 检查内存使用,发现超过了阈值;
  2. 进入淘汰流程,确定还需要释放多少内存;
  3. 从数据库字典中随机采样,默认抽取 5 个 key;
  4. 逐个计算空闲时间,选空闲时间最长的一个作为本轮淘汰候选;
  5. 删除这个 key,回收内存;
  6. 再次检查内存,如果还不够,继续下一轮淘汰;
  7. 内存降到阈值以下后,才开始执行原来的写命令。

每一步的内存释放量、采样次数都是有限度的,Redis 不会因为淘汰陷入死循环。你可以把“每次采样 5 个”当成一个折中:样本太少容易误杀热 key,样本太多又会让淘汰过程变慢,影响正常的命令响应。

2.4 为什么说它是“近似”的:一个直观例子

假设实例里有 100 万个 key,内存快要爆了。标准 LRU 会扫一条全局链表,立刻知道最冷的是哪一个。而 Redis 每次只随机看 5 个,这 5 个里面最冷的是谁,就先删谁。如果最冷的 key 没被抽中,那这次淘汰就会用“次冷”的顶替。样本越大,越逼近全局最优;但这里有个边际效应,超过一定值后准确率提升不明显,CPU 消耗却线性增长。

Redis 默认把样本数定在 5,并不是拍脑袋,而是官方实测权衡过的结果。官方文档里有组参考数据,样本数为 5、10、20 时,淘汰质量和标准 LRU 之间的差距会递减,但 20 的 CPU 开销明显更高。所以大多数业务场景,5 已经够用;对淘汰准确性极其敏感,或者实例本身命中率要求极高的场景,才建议提升到 10 左右。


3. 版本演进:参数池、时钟精度和 LFU 的加入

3.1 Redis 2.8 时代的粗糙实现

继续说历史。Redis 2.8 之前,近似 LRU 的可靠性比现在要弱不少。一方面是时钟分辨率不高,另一方面是每次淘汰只看单一轮采样里的最优值,缺少历史候选记录。结果就是,如果本次采样恰好抽到了几个其实挺新的 key,淘汰动作就会在“矬子里面拔将军”,最终删掉的 key 可能并不是真正最冷的那批。

那时候调优手段也有限,只能靠加大maxmemory-samples暴力提升准确度。但样本数增大后,每次淘汰都要做更多空闲时间计算,对延迟的影响也更明显。

3.2 Redis 3.0 引入 LRU 候选池

Redis 3.0 做了一件很关键的优化:引入一个大小为 16 的淘汰候选池(eviction pool)。淘汰候选池的核心思路是,把上一轮淘汰时采样到的 key 暂时保留下来,等下一轮继续比较,让“旧候选”和“新样本”不断竞争。这样一来,即使某轮采样没碰到最冷的 key,最冷的 key 也有机会在后续轮次里被选中。

具体流程是这样的:

  1. 每轮采样maxmemory-samples个 key;
  2. 把它们的空闲信息加入候选池;
  3. 候选池只保留空闲时间最大的 16 个 key;
  4. 真正删除时,从候选池里选出空闲时间最长的那个。

这相当于把一次随机变成了“多次随机 + 保留最佳结果”,准确性提升非常明显。更重要的是,它不增加每次访问的开销,只增加淘汰发生时的计算量,而淘汰本身就不是高频动作,所以这笔账很划算。

3.3 Redis 4.0 之后的 LFU 选项

接近 LRU 的演进自然不止于 LRU 本身。Redis 4.0 又加入了 LFU(Least Frequently Used,最不频繁使用),策略值包括allkeys-lfu和volatile-lfu。LFU 的核心思想是:如果一个 key 以前访问很频繁,即使最近一小段时间没被访问,也不该立刻淘汰;LRU 更看重“最近一次访问时间”,而 LFU 更看重“长期访问热度”。

LFU 在 Redis 里的实现很有意思,它复用了同一个 24 bit 字段:高 16 位存一个对数计数器,低 8 位存衰减时间。计数器还会随着时间推移衰减,避免那种“古代很热,但现在已经冷透”的 key 永远占着坑。具体参数可以通过lfu-log-factor和lfu-decay-time配置调节。

但这篇文章聚焦在 LRU,所以只说一句结论:如果业务中存在明显的流量热点轮换,LRU 更合适;如果热点相对稳定,只是偶有波动,LFU 更抗“抖动”。实际选择时,优先 LRU,遇到特定淘汰倒挂现象再考虑切 LFU。

3.4 版本演进给我们的启示

回头看这套演进路线,核心诉求始终是两条:内存占用要小、淘汰判断要准。标准 LRU 太贵,纯随机采样太糙,于是 Redis 用“24 bit 时间戳 + 随机采样 + 候选池”构成了一整套工程方案。它不是单独一个算法,而是一个由多个机制拼起来的最优解。

做技术选型的时候,经常有人问我“Redis 的 LRU 是不是标准 LRU”,我会明确回答:不是,但工程上更可靠。理解了这一点,后续所有调优动作才有意义。


4. 实操:从配置到压测,亲手验证 LRU 淘汰行为

4.1 最小配置清单

动手之前先把要用的参数列出来:

  • maxmemory:内存上限,可以写带单位的大小,比如128mb;
  • maxmemory-policy:淘汰策略,建议先设allkeys-lru来观察;
  • maxmemory-samples:采样数,默认 5,调优时可以临时改成 10;
  • maxmemory-clients(Redis 7.0 之后):对特定客户端连接设置更细的内存限制,这里先不管。

配置可以在redis.conf里写,也可以运行时用CONFIG SET动态调整。线上环境我更建议先用CONFIG SET做灰度,确认稳定后再固化到配置文件。

# 查看当前相关配置 redis-cli CONFIG GET maxmemory redis-cli CONFIG GET maxmemory-policy redis-cli CONFIG GET maxmemory-samples # 运行时调整为 allkeys-lru redis-cli CONFIG SET maxmemory-policy allkeys-lru

分配内存为0或没有配置时,代表不限制内存使用。但 64 位实例下不设上限很危险,操作系统内存耗尽后会触发 OOM Killer,直接把 Redis 进程杀掉,属于“跑得好好地忽然就没了”的典型原因。所以只要做缓存场景,就一定要设maxmemory,而且要留出系统和其他进程的安全余量。

4.2 在本地环境构造一个可复现的淘汰现场

先用一个非常小的内存上限来模拟淘汰。假设本地实例运行在 6379 端口,我打算把上限压到只有几十 MB,然后用脚本灌入一批 key。

# 设置一个很小但足够系统运行的内存上限 redis-cli CONFIG SET maxmemory 64mb redis-cli CONFIG SET maxmemory-policy allkeys-lru

接着批量写入,最后触发淘汰。注意别把实例搞崩,64mb 对于空 Redis 是安全的,系统本身还有空闲。

import redis r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) for i in range(200000): r.set(f"hot-key-{i}", "v" * 1024) if i % 1000 == 0: # 模拟大量访问,让部分 key 变热 r.get(f"hot-key-{i % 5000}") print(r.info("memory")["used_memory"]) print(r.info("stats")["evicted_keys"])

写入 20 万个 1KB 的 key 大约是 200MB 数据,远超 64mb 上限,所以中途必然触发淘汰。每次循环里还会主动 GET 一部分 key,让它们在 LRU 时钟上留下更新鲜的痕迹。

跑完后看两个指标:

  • used_memory:应该被压在 64mb 附近;
  • evicted_keys:大于 0,说明淘汰发生过。

如果想看具体某个 key 的空闲状态,用:

redis-cli DEBUG OBJECT hot-key-1000

输出里会包含lru:和lru_seconds_idle:两项。其中lru_seconds_idle就是距离上次访问过去了多少秒。调试时如果发现某个 key 明明业务一直在读,但lru_seconds_idle却异常大,就要怀疑是不是访问没有真正触发 Redis 层的读操作,或者存在多个实例、不同 DB 的混淆。

4.3 用 redis-benchmark 观察淘汰对 QPS 的影响

redis-benchmark可以用来模拟压力,观察淘汰发生时实例的吞吐变化。一条常用命令像是:

redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 100000 -c 50 -r 100000

-r 100000表示 key 的随机后缀范围是 0 到 99999,这样压测时会产生大量不同的 key。如果maxmemory很小,压测过程中就会持续淘汰,后面你可以在redis-cli --stat的实时输出里看到evict计数不断上涨。

redis-cli --stat

这个命令每秒刷新一次,显示ops、hits、misses、evict等字段。它特别适合现场观察:一开始 hits 可能不错,内存打满之后 miss 上升,因为旧 key 被淘汰、新 key 需要重新加载。如果这时候命中率跌得厉害,基本说明缓存的有效容量比物理上限小,需要调大maxmemory,或者优化 key 大小。

4.4 调优maxmemory-samples的节奏

把样本数从默认 5 改到 10,通常会让淘汰结果更接近标准 LRU。实际操作建议分三步走:

  1. 先不调,运行几天记录命中率和evicted_keys基线;
  2. 调整样本数为 10,观察一周;
  3. 用命中率、淘汰总量、平均延迟三个指标对比,确认收益之后再决定是否保留。

如果你的实例淘汰频繁发生,且每次淘汰后马上又有大量 key 被重新载入,那说明淘汰质量不够好,可以尝试调高样本数。如果淘汰频率本来就不高,强行调高只会白白增加 CPU 开销。

另一个容易被忽略的参数是maxmemory-policy的运行时切换。你可以在流量低峰期用CONFIG SET maxmemory-policy allkeys-lfu观察几天,对比同样的业务压力下evicted_keys是否下降。如果下降了,说明业务冷热模式更契合 LFU。不过注意,策略切换是全局生效的,最好配合监控快速回滚。


5. 高频问题与避坑指南

5.1 备一下常见问题速查表

实践中最常遇到的情况,我整理成了下面这个表,排查时可以直接对号入座。

现象可能原因处理方式
写入报OOM command not allowedmaxmemory-policy是noeviction,且内存已满改成allkeys-lru或volatile-lru
热 key 被淘汰,冷 key 存活采样数太小,或随机性导致误判适当增大maxmemory-samples,观察命中率
明明设置了过期时间,还是被删用的是allkeys-lru,无 TTL 的 key 也会被淘汰切换为volatile-lru,或接受淘汰行为
DEBUG OBJECT显示的 idle 时间一直很大访问可能没落到这个实例,或存在多路复用混淆确认客户端连接实例,检查是否有多从读
实例运行 100 多天后淘汰逻辑异常怀疑 LRU 时钟回绕确认版本大于 3.0,必要时升级
主从环境下从节点也出现内存超限从节点自身也参与淘汰或复制数据增长过快检查从节点maxmemory配置,合理调大或优化主节点淘汰

5.2 时钟回绕和操作系统时间调整的影响

LRU 时钟虽然做了 194 天回绕处理,但如果系统时间被手动跳变、NTP 大幅校准,还是会因为全局时钟的非单调性导致空闲时间计算短暂失真。Redis 对此能做的有限,因为它拿到的底层时间源就是gettimeofday这类系统调用。

好在这个影响窗口很短,最多几十秒,而且只是让淘汰候选稍微偏离,不会导致数据损坏。真正要警惕的是那种大规模时间回拨配合低版本 Redis 的情况,所以线上实例尽量保持在 3.0 之后的版本,遇到时间同步异常时优先切换手动调整或配置合理的 NTP 策略。

5.3 空淘汰和主从传播

有个细节很多人不知道:当使用volatile-lru且实例里没有带 TTL 的 key 时,内存满了 Redis 依然会报 OOM 错误,因为它找不到任何可淘汰对象。这种情况不是策略配置错了,而是业务结构本身没给淘汰留下空间。解决办法要么给一批 key 补上合理的过期时间,要么切换策略到allkeys-lru。

还有一个易踩的坑在主从架构里。主节点淘汰一个 key 后,会把这个删除动作同步给从节点,从节点会跟着删。如果从节点的maxmemory设置得比主节点还小,那从节点可能先于主节点触发淘汰,此时即使策略和主节点一样,从节点的淘汰候选也会和主节点出现差异。更严重的情况下,主节点还没删,从节点先删了,再等到主节点发来删除命令,就会出现“明明该存在却查不到”的现象。

所以主从架构中,从节点的maxmemory一定要大于或等于主节点,或者干脆不给从节点设置淘汰策略,让它完全跟随主节点命令。这个原则是很多缓存事故的止血点。

5.4 一套我自己在用的排查顺序

遇到“缓存不命中率突然升高”的问题,我个人习惯按下面这个顺序排查,每次基本都能定位:

  1. 先INFO stats看evicted_keys是否突增,确认是淘汰导致,还是业务流量导致的正常 miss;
  2. 再INFO memory看used_memory和maxmemory的差值,如果used_memory长期贴着上限,说明容量很紧张;
  3. 用redis-cli --stat实时观察,判断淘汰当前是否在持续发生;
  4. 抽查几个热 key 的DEBUG OBJECT,看lru_seconds_idle是否异常;
  5. 最后再根据业务特征决定是调大内存、调高采样数,还是更换淘汰策略。

这套流程的好处是把“玄学”一步步变成可量化检查。中间少一步都可能误判,尤其是第 4 步,它能看到单个 key 的真实状态。


我自己的经验里,LRU 在 Redis 中最大的价值不是让淘汰多精准,而是让淘汰这件事变得足够廉价、足够自动。真正需要在意的永远是业务侧的热点模型和容量水位。参数调优能解决一部分问题,但不可能解决所有问题;理解了近似 LRU 的本质,遇到“为什么删了我这个 key”的质疑时,你就能给出一个有理有据的回答,而不是打开日志一通乱翻。

最后再分享一个小技巧:排查淘汰问题时,别只盯着evicted_keys总量,把它和时间段结合看,哪个小时段淘汰骤增,往往那个时段对应的流量高峰和热点切换就是你真正要关注的点。这种“指标+时间窗口”的组合分析,比单纯看一个全局数字有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 20:11:06

诘问·协议·生成:内容团队如何打牢协作根基

《元创力》纪实录写到第六卷&#xff0c;很多人问我&#xff0c;这一卷为什么叫“根基”。说实话&#xff0c;这个命名不是提前想好的&#xff0c;而是项目走到第五卷末尾时&#xff0c;我们不得不承认一个尴尬的现状&#xff1a;生成速度越来越快&#xff0c;内容管线越来越顺…

作者头像 李华
网站建设 2026/10/16 12:32:40

微电网并联逆变器功率坐标变换:破解P/Q耦合与功率分配难题

微电网里并联逆变器的功率分配问题&#xff0c;做控制的人多少都跟它较过劲。多台逆变器并联&#xff0c;调压调频、均流限流、环流抑制、模式切换&#xff0c;每一环都有讲究。常规下垂控制实现简单、无互联通信&#xff0c;但实际跑起来总会有偏航&#xff1a;线路阻抗不匹配…

作者头像 李华
网站建设 2026/10/15 6:22:08

Windows下Python库安装避坑指南:从环境准备到版本验证

在Windows上装Python库&#xff0c;装过的人都知道&#xff0c;真正的坑往往不在pip install那一行命令&#xff0c;而是在装之前的环境检查&#xff0c;和装完之后的版本验证。最近我在给同事讲解phy这个Python包的时候&#xff0c;把整个过程重新梳理了一遍&#xff0c;发现只…

作者头像 李华
网站建设 2026/10/16 7:51:44

Redis入门到精通(十一),Redission操作Redis

一. 初识Redission 前面我们讲了Redis的三大常见客户端中的Jedis和Lettuce&#xff0c;今天就跟大家聊聊这个Redisson。那Redisson是什么呢&#xff1f;Redisson是一个在Redis的基础上实现的Java内存数据网格&#xff08;In-Memory Data Grid&#xff09;&#xff0c;它充分利用…

作者头像 李华
网站建设 2026/10/16 7:17:21

从D4RL到NewRL:离线强化学习评估为何失真及如何构建真实场景基准

做离线强化学习研究的同学&#xff0c;这两年估计都有一个共同体感&#xff1a;在D4RL这类标准基准上刷出来的分数越来越好看&#xff0c;可一旦把模型放到真实系统里试运行&#xff0c;各种问题就冒出来了——状态观测有噪声、奖励迟迟不来、执行器还经常延迟&#xff0c;最后…

作者头像 李华
网站建设 2026/10/14 18:18:29

实时大数据平台元数据管理实战:从Schema演化到血缘追踪

实时大数据平台的元数据管理有多难&#xff1f;我这里想先抛一个场景&#xff1a;你负责的实时数仓跑了大半年&#xff0c;上游业务表某天加了个字段&#xff0c;按说这是常用操作&#xff0c;结果当天晚上实时任务大面积报错、重启&#xff0c;Kafka Topic里的数据堆积到几千万…

作者头像 李华