1. 从一道"看起来很简单"的面试题说起
Redis 是怎么淘汰 key 的——这个问题我第一次被问到时,脑子里蹦出来的答案只有三个字:过期删。结果对面追问一句"那如果内存满了呢",我当场卡壳。后来带新人、做技术评审、排查线上内存告警,才发现这块知识点的坑远比想象中深:过期删除和内存淘汰是两套完全不同的机制,很多人把它们混成一件事,配置的时候随手写个maxmemory-policy allkeys-lru就上线,等到缓存命中率掉到 60% 才回来翻文档。
这篇内容围绕分布式缓存里最容易被忽略、又最容易背锅的一个环节展开:key 到底是怎么被清掉的。适合谁看?写过 Redis 基本读写、能独立部署一个实例,但面对"内存突然打满""过期 key 没被删掉""淘汰策略选哪个"这类问题心里没底的同学。如果你已经能说清楚expire和maxmemory各自管什么,那这篇可以当作一次系统性复盘,我把参数计算、策略选型、线上排查的完整链路都摊开讲一遍。
先把最容易混淆的概念摆正,后面所有内容都建立在这个前提上:
| 机制 | 触发条件 | 作用对象 | 核心配置 | 结果 |
|---|---|---|---|---|
| 过期删除 | key 到达 TTL 时间点 | 设置了过期时间的 key | expire/pexpire/setex | key 变为不可读,空间回收 |
| 内存淘汰 | used_memory 超过 maxmemory | 按策略挑选的候选 key | maxmemory+maxmemory-policy | 主动删除 key 腾出空间 |
这两条线是并行运行的。一个 key 可能既没过期、也没被淘汰,但因为内存打满被踢掉;也可能 TTL 早到了,但内存远没满,理论上该删——可实际上 Redis 并没有立刻删它。接下来我把这些"看起来不合常理"的地方一个个拆开。
2. 过期删除的底层设计:为什么不能一到期就删
2.1 惰性删除与定期删除的组合拳
最朴素的想法是:给每个 key 挂一个定时器,到点就触发删除。Redis 的作者早年明确否掉了这条路——如果十万个 key 都设了同一个时间点过期,那一刻 CPU 会被定时器回调打满,而且每个定时器本身要占用内存(Redis 内部用最小堆维护的话,单个节点的开销并不低)。这是典型的"用空间和调度成本换精确性",在缓存场景里完全不划算。
所以 Redis 用的是惰性删除 + 定期删除的组合:
惰性删除的意思是,key 过期之后不主动处理,等你下次访问它的时候,顺手检查一下 TTL 是否已过,过了就删掉并返回空。好处是零额外开销,坏处也明显——如果一个 key 过期后永远没人访问,它就赖在内存里不走。这在"冷数据加过期时间"的场景下会造成实打实的空间浪费。
定期删除是用来擦屁股的。Redis 默认每秒执行 10 次(hz配置,默认 10,即每 100ms 一次)后台任务,每次从设置了过期时间的 key 集合里随机抽一批做检查,把过期的删掉。注意两个关键词:随机、一批。它不是全量扫描,因为全量扫描在千万级 key 的情况下会直接把主线程卡住。
这里我补充一个很多人不知道的细节:这个随机采样不是抽一次就完事,而是有一个循环逻辑——如果本轮抽样的 key 里过期比例超过 25%,就继续再抽一轮,直到比例降下来或者达到时间上限(默认单次任务不超过 25% 的 CPU 时间)。这个设计挺巧妙的,它让"过期 key 密集"的场景能被快速清理,而"过期 key 稀疏"的场景不会浪费太多 CPU。你可以理解为:先随机摸一把,摸到的坏苹果多,就再多摸几把。
每次抽样的数量由active-expire-effort(早期版本叫hz相关参数)控制,默认值 1,对应每次抽样 20 个 key。调大这个值会提高清理的激进程度,代价是 CPU 占用上升。我的经验是:除非你确认过期 key 堆积已经影响到内存,否则不要动这个参数,默认值在绝大多数业务下够用。
2.2 从节点上的过期语义差异
这是个容易被忽略但很致命的点。在主从架构里,从节点不会主动删除过期 key。从节点的处理逻辑是:等主节点把 DEL 命令同步过来,再执行删除。在此之前,如果客户端读从节点,可能会读到逻辑上已经过期的数据。
为什么这么设计?因为主从之间要保持数据一致性,如果从节点自作主张删了 key,主节点还没删,那主从数据就分叉了。所以规则是:过期判定跟随主节点,删除动作由主节点驱动。
这带来两个实际影响。第一,如果你用读写分离,读到过期数据的窗口是存在的,对强一致要求高的业务要留意。第二,主节点在淘汰 key 时,同样会把删除命令同步给从节点,所以淘汰动作在主从上是一致的,不用担心。
我踩过一次坑:某个业务的从节点内存比主节点涨得快,查了半天发现是从节点上堆了一批"逻辑已过期但还没收到 DEL"的 key。后来把过期时间打散、避免大量 key 同一时刻过期,问题就缓解了。
注意:Redis 4.0 之后支持
lazyfree系列配置,可以把大 key 的删除操作放到后台线程,避免 DEL 阻塞主线程。配置lazyfree-lazy-expire yes后,过期删除也走异步路径。大 key 场景强烈建议打开。
2.3 过期时间的存储与精度问题
过期时间存在哪?在 Redis 内部,每个数据库有一个expires字典,key 指向具体的过期时间戳(毫秒级 Unix 时间)。也就是说过期时间本身也占内存,一个 key 设了 TTL,大概多占几十字节。千万级 key 全部设过期时间,这部分开销也是百万级字节的量级,容量规划时要算进去。
精度方面,expire是秒级,pexpire是毫秒级。听起来毫秒级更精确,但实际上定期删除是 100ms 一轮的节奏,所以毫秒级的过期时间在实践中大约有 100ms 量级的误差。如果你的业务依赖"到点必须立刻不可见"的严格语义,Redis 的过期机制给不了这个保证——它只保证"到点之后某个时刻会被清理,并且在清理前通过惰性删除保证读不到"。
3. 内存淘汰策略:八种策略到底怎么选
3.1 策略全表与选择逻辑
内存淘汰才是"内存满了怎么办"的答案。maxmemory-policy一共有八种取值,我按"淘汰范围"和"淘汰依据"两个维度整理成表,这个表建议直接存下来:
| 策略 | 候选范围 | 淘汰依据 | 适用场景 |
|---|---|---|---|
| noeviction | 无 | 不淘汰,写操作直接报错 | 数据不可丢,如队列、配置中心 |
| allkeys-lru | 所有 key | 最近最少使用 | 通用缓存,首选 |
| allkeys-lfu | 所有 key | 访问频率最低 | 热点集中、长尾明显的缓存 |
| allkeys-random | 所有 key | 随机 | 访问分布均匀,几乎不用 |
| volatile-lru | 设了 TTL 的 key | 最近最少使用 | 缓存与持久数据混布 |
| volatile-lfu | 设了 TTL 的 key | 访问频率最低 | 同上,热点型 |
| volatile-random | 设了 TTL 的 key | 随机 | 极少用 |
| volatile-ttl | 设了 TTL 的 key | 剩余时间最短优先 | 短期凭证、验证码类 |
选型其实就三步判断:
第一步,问自己内存满了能不能报错。如果这个实例上存的是绝对不能丢的东西(比如简易队列、分布式锁的元数据混在里面),选noeviction,让写操作失败,由应用层感知并处理。但说实话,纯缓存实例用noeviction是自找麻烦,线上见过太多次内存打满后所有写请求报OOM command not allowed,业务方还以为是网络问题。
第二步,问是不是所有 key 都可以淘汰。如果实例里混了缓存数据和"必须保留"的数据(比如某些常驻的配置、排行榜的兜底数据),用volatile-*系列,只淘汰设了 TTL 的。但这里有个大坑:如果所有 key 都没设 TTL,volatile 系列等于不淘汰,内存打满后行为和noeviction一样。我见过有团队配了volatile-lru但业务代码全都没设过期时间,上线三个月后内存爆掉。
第三步,问访问分布是偏热点还是偏均匀。绝大多数互联网业务的访问分布都是长尾的——少数热点 key 承担大部分流量。这种分布下,allkeys-lru和allkeys-lfu都行;如果热点极其集中、且存在"周期性批量扫描"把冷 key 刷成热 key 的情况,LFU 会更稳,因为它记录的是访问频次而不是最近一次访问时间,不会被一次批量任务带偏。
3.2 LRU 与 LFU 的实现差异,以及近似算法的意义
这里必须讲清楚一件事:Redis 的 LRU 不是精确 LRU。教科书上的 LRU 需要双向链表 + 哈希表,每次访问都要移动节点,在 Redis 这种高并发场景下,每次读写都改链表开销太大,还得加锁。
Redis 的做法是采样近似:每个对象(redisObject)里有一个 24 位的lru字段,记录最近一次访问的时间戳(秒级精度)。当需要淘汰时,随机抽maxmemory-samples个 key(默认 5 个),从中挑lru值最旧的那个删掉。
默认 5 个样本的近似程度其实一般。官方给过一组数据:samples 为 5 时,淘汰效果接近精确 LRU 的 90% 左右;调到 10 接近 99%。代价是每次淘汰多花一点 CPU。我的建议是内存敏感、淘汰频繁的实例调成 10,配置项叫maxmemory-samples。这个改动成本极低,收益却很实在。
LFU 是 Redis 4.0 引入的,把那个 24 位字段拆成了两段:高 16 位存访问时间(分钟级精度),低 8 位存访问计数器(叫counter,最大值 255)。这里面有个精妙的设计叫对数计数器:计数器不是每次访问加 1,而是按概率递增,值越大越难加。这样做的目的是让"访问 1000 次"和"访问 10000 次"在有限的 8 位空间里能区分开,避免计数器迅速饱和。
计数器还会随时间衰减,衰减速率由lfu-decay-time控制(默认 1,单位分钟)。也就是说,一个曾经很热但最近没人访问的 key,它的 counter 会慢慢降下来,重新变成可淘汰对象。这个衰减机制是 LFU 比 LRU 更适合"热点会漂移"场景的原因。
还有个参数lfu-log-factor(默认 10),控制计数器增长的难度。调大它,计数器增长更慢,区分度更高但饱和更晚。这个参数我基本没动过,默认值在绝大部分场景下是合理的。
3.3 采样淘汰的完整链路
把上面串起来,一次内存淘汰的完整过程是这样:
- 客户端写入命令到达,Redis 先检查
used_memory是否超过maxmemory(注意:这里用的是 Redis 自己统计的内存,不含 fork 出来的子进程开销、不含内存碎片外的部分)。 - 如果超了,进入淘汰流程,根据
maxmemory-policy确定候选池(全部 key 或带 TTL 的 key)。 - 从候选池随机采样
maxmemory-samples个 key。 - 按策略比较这若干个 key 的
lru字段或counter值,选出最该被淘汰的那个。 - 删除该 key(同步 DEL 或异步 UNLINK,取决于
lazyfree配置),同步给从节点。 - 重复步骤 2-5,直到内存降到
maxmemory以下,或者找不到可淘汰的 key(此时返回 OOM 错误)。
有个细节值得注意:淘汰是在命令执行前触发的(准确说是命令处理流程里检查内存),所以它会给每个写命令增加额外延迟。如果你的淘汰很频繁,P99 延迟会明显抖动。这也是为什么要尽量避免"内存长期贴着 maxmemory 跑"的状态——那是把淘汰开销平摊到了每一次写请求上。
4. 参数怎么算:maxmemory 到底设多少
4.1 一个可落地的容量计算公式
这块是实操里最容易拍脑袋的地方。很多人直接maxmemory设成机器内存的 80%,这个比例在某些场景下是危险的。
先看内存都被什么占了:
- 数据本身:key + value +
redisObject头(16 字节)+ 字典项开销。一个简单的 string 类型 key-value,实际开销大约是 value 长度的 1.2 到 1.5 倍,短 key 短 value 时开销比例更高。 - 过期字典:设了 TTL 的 key,每个额外约 30-50 字节。
- 复制缓冲区:如果有从节点,
repl-backlog默认 1MB,但高峰期的输出缓冲区可能几十 MB。 - 客户端缓冲区:每个连接有输入输出缓冲,连接数多的时候不可忽略。
- AOF 缓冲区、Lua 脚本、模块内存等杂项。
- 内存碎片:
mem_fragmentation_ratio通常在 1.1 到 1.5 之间,极端情况更高。
所以公式大致是:
maxmemory = 物理内存 × 0.75 - 复制缓冲区预留 - 客户端缓冲预留 - 碎片余量举个具体例子:一台 16GB 的机器,单实例部署,有 1 个从节点,预计连接数 2000。
- 物理内存 16GB,先打 0.75 折得 12GB,这是留给 Redis 的总盘子。
- 从节点复制缓冲区按峰值预留 512MB。
- 2000 连接 × 平均 20KB 缓冲 = 40MB,按 100MB 预留。
- 碎片按 25% 计,在 12GB 里已经隐含(0.75 这个系数本身就含了碎片和系统开销的余量)。
那么maxmemory可以设到 12GB - 0.5GB - 0.1GB ≈ 11.4GB,取整 11GB 比较稳妥。千万别设 15GB,因为一旦触发 fork(做 RDB 或 AOF 重写),COW 机制会让内存翻倍上涨,那时候 OOM killer 就会来敲门。
一个重要提醒:
maxmemory统计的是 Redis 逻辑内存(used_memory),不包含 fork 出的子进程和 COW 复制的页。所以设得太贴物理内存上限,在持久化时会出大事。
4.2 大 key 与过期 key 堆积的连锁反应
容量规划还有一个隐蔽因素:大 key 的存在会放大淘汰的代价。假设你有一个 500MB 的 hash,被 LRU 选中要淘汰,同步 DEL 会阻塞主线程几百毫秒甚至更久。这期间所有请求都堵着。
所以第一,开lazyfree-lazy-eviction yes、lazyfree-lazy-expire yes,让淘汰和过期删除走后台线程。第二,从根上避免大 key,单个 value 超过 10KB 就要审视,集合类型元素数超过 5000 就要考虑拆分。
过期 key 堆积则是另一个连锁问题:如果业务大量 key 在同一时刻过期(比如凌晨批量导入时统一设 24 小时 TTL),那么到期那一刻,定期删除的任务量会陡增,CPU 出现尖刺;同时这些 key 在没被清掉之前仍占内存,used_memory虚高,可能触发不必要的淘汰。解决办法是给 TTL 加随机抖动,比如基础 24 小时 + 随机 0 到 1800 秒,把过期时间打散到一个时间窗内。这个改动只有一行代码,效果立竿见影。
5. 实操验证:把淘汰行为跑出来看
5.1 搭一个能快速触发淘汰的实例
空谈不如实测。下面这套流程可以在本地几分钟内复现完整的淘汰过程,我用它给新人演示过很多次。
启动一个限制内存的小实例:
redis-server --port 6380 \ --maxmemory 20mb \ --maxmemory-policy allkeys-lru \ --maxmemory-samples 10 \ --save "" \ --appendonly no关掉持久化是为了排除干扰,20MB 内存方便快速打满。连上去,先看几个关键指标:
redis-cli -p 6380 127.0.0.1:6380> config get maxmemory 127.0.0.1:6380> config get maxmemory-policy 127.0.0.1:6380> info memoryinfo memory里重点关注used_memory_human、maxmemory_human、mem_fragmentation_ratio,以及evicted_keys(累计淘汰数量)。这个计数器是我们的观测核心。
然后灌数据。用redis-benchmark或者一段简单脚本都行,注意 value 要足够大,才能快速撑满:
redis-benchmark -p 6380 -n 100000 -d 1024 -t set -r 100000 -q-d 1024表示每个 value 1KB,-r 100000表示 key 在 10 万个随机后缀里选,这样能保证 key 不重复。跑完之后再看:
127.0.0.1:6380> info stats | grep evicted evicted_keys:61947 127.0.0.1:6380> dbsize (integer) 14320evicted_keys有值,说明淘汰真的发生了;dbsize剩下 1.4 万左右,说明 20MB 大概能装这么多 1KB 的数据。到这一步,你已经亲眼看到了"内存满了自动踢 key"的全过程。
5.2 验证 LRU 近似的实际效果
接着验证采样近似的准确性。做法是造一批"明显该被淘汰"的冷数据,再看它们是否真的被优先清掉。
先清空,灌入 5000 个 key 作为"冷数据",用一个固定的前缀:
redis-cli -p 6380 flushall for i in $(seq 1 5000); do redis-cli -p 6380 set "cold:$i" "$(head -c 512 /dev/zero | tr '\0' 'x')" > /dev/null done再用另一个前缀灌入 20000 个 key,并且持续访问其中一部分,把它们"烤热":
for i in $(seq 1 20000); do redis-cli -p 6380 set "hot:$i" "..." > /dev/null done for round in $(seq 1 5); do for i in $(seq 1 1000); do redis-cli -p 6380 get "hot:$i" > /dev/null done done然后再灌一批新数据触发淘汰,最后统计cold:和hot:前缀各剩多少。如果 LRU 生效,cold:的存活率应该显著低于被反复访问的hot:前 1000 个。
用redis-cli --scan配合 shell 统计:
redis-cli -p 6380 --scan --pattern "cold:*" | wc -l redis-cli -p 6380 --scan --pattern "hot:*" | wc -l这里要注意,--scan本身是游标遍历,不会像keys *那样阻塞,但 key 数量多的时候也要跑一会儿。实测时我把maxmemory-samples分别设成 3 和 10 对比过,10 的情况下冷数据存活率明显更低,验证了"采样数越大越接近精确 LRU"这个结论。
提醒:这个实验用的是一次性命令循环,
redis-cli每次调用都有连接开销,真实压测请用redis-benchmark或客户端 pipeline,否则瓶颈在进程创建上,测不出 Redis 的真实性能。
5.3 验证过期 key 的实际清理时机
这个实验更直观。设一个 key 10 秒后过期,然后观察它在不同时间点的状态:
redis-cli -p 6380 set demo "v" ex 1010 秒内ttl demo返回剩余秒数。超过 10 秒后分两种情况:
- 如果你访问它(
get demo),返回nil,同时这个 key 被惰性删除。再exists demo返回 0,dbsize减一。 - 如果你不访问它,这个 key 在逻辑上已过期,但可能仍占着内存,直到定期删除任务扫到它。你可以用
info keyspace看expires字段,或者用dbsize观察是否下降——注意dbsize统计的是物理存在的 key,过期的也算在内。
想观察得更清楚,可以把hz调小(比如设成 1,即每秒一轮),然后重复实验,你会发现过期 key 在内存里赖着的时间变长。反过来把active-expire-effort调大,清理会积极很多。这个对照实验能让"惰性删除 + 定期删除"从概念变成看得见的行为。
6. 线上排查实录:四个真实场景的定位路径
6.1 内存越用越多但 dbsize 没涨
现象:used_memory持续爬升,dbsize稳定,业务没报错。
排查顺序我一般这么走。先看info memory里的碎片率:
redis-cli info memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"如果mem_fragmentation_ratio超过 1.5,说明碎片严重,数据删了但内存没还给系统。Redis 用的是 jemalloc,默认不会主动归还。处理办法是开activedefrag yes(4.0+ 支持),配合active-defrag-ignore-bytes 100mb、active-defrag-threshold-lower 10等参数控制激进度;或者低峰期重启实例。如果碎片率正常,那就要查是不是有大 key 在缓慢增长,用redis-cli --bigkeys扫一遍。
还有一种情况是输出缓冲区堆积。info clients里看client_recent_max_output_buffer,如果很大,说明某个客户端在狂拉数据但消费不过来,可能是慢客户端。这时候可以设client-output-buffer-limit限制。
6.2 淘汰很凶但业务反馈缓存命中率还行
这个场景有点反直觉,但很常见。evicted_keys每分钟涨几万,业务侧却没什么感知。
原因通常是:淘汰掉的都是本来就不该长期驻留的冷 key。这种实例其实处于健康状态——内存被充分利用,淘汰机制正常运转。真正要警惕的是另一种情况:evicted_keys很高,同时命中率也在掉,说明淘汰把热 key 也踢了。
定位方法是对比keyspace_hits和keyspace_misses:
redis-cli info stats | grep -E "keyspace_hits|keyspace_misses"命中率 = hits / (hits + misses)。如果这个值从 95% 掉到 80%,同时淘汰量很高,那八成是内存给少了,或者策略选错了。如果确实存在明显热点,从 LRU 换成 LFU 试一周,通常会有改善。
6.3 大量 key 同时过期引发的 CPU 尖刺
现象:每天固定时间点(比如凌晨 2 点),监控上 Redis 的 CPU 出现明显尖刺,P99 延迟抖一下。
这就是典型的批量过期。定位手段:看info stats里的expired_keys变化速率,在尖刺前后拉个曲线,如果它在这个时间点陡增,基本确认。
解决分两层。应用层给 TTL 加随机抖动,这是治本。实例层如果短期内改不了代码,可以适度降低hz(但不能降太多,会影响其他后台任务),或者把过期时间分散到多个不同时长的 key 上。还有一种土办法是把批量导入拆成多个批次,每批用不同的基础 TTL,效果也不错。
6.4 volatile 策略下内存打满不淘汰
这个坑我单独拎出来讲,因为太隐蔽了。
现象:配了volatile-lru,内存满了,但evicted_keys是 0,写请求开始报OOM command not allowed when used memory > 'maxmemory'。
原因只有一个:候选池是空的,也就是没有任何 key 设置了过期时间。volatile-*系列只从有 TTL 的 key 里淘汰,一个都没设,可淘汰集合就是空的。
验证方法很简单:
redis-cli info keyspace # db0:keys=1000000,expires=0,avg_ttl=0expires=0就是铁证。解决要么给缓存 key 统一加上 TTL(本来就是缓存,应该加),要么把策略改成allkeys-lru。我倾向后者配合代码规范双管齐下:策略用allkeys-lru兜底,同时代码层面强制缓存 key 必须有过期时间,通过代码审查和 lint 规则保证。
6.5 排查速查表
把上面这些整理成一张对照表,出问题时按图索骥:
| 现象 | 优先检查 | 可能原因 | 处理动作 |
|---|---|---|---|
| 内存涨但 dbsize 稳定 | mem_fragmentation_ratio | 内存碎片 | 开 activedefrag 或低峰重启 |
| 写请求报 OOM | maxmemory-policy+expires | volatile 策略无可淘汰 key | 改 allkeys-lru 或补 TTL |
| 命中率骤降 | 淘汰量与 hits/misses | 热 key 被淘汰 | 加内存或换 LFU |
| 固定时间 CPU 尖刺 | expired_keys曲线 | 批量同时过期 | TTL 加随机抖动 |
| 主从内存不一致 | 从节点expires数量 | 从节点被动等 DEL | 正常现象,关注逻辑过期窗口 |
| 删除操作卡顿 | 是否有大 key | 同步 DEL 阻塞 | 开 lazyfree,拆大 key |
7. 几个容易答错的细节与我的使用习惯
先澄清一个高频误解:Redis 的淘汰和过期都不是"定时精确"的。任何"我设了 60 秒,第 61 秒它必须消失"的假设都不成立。正确的理解是:60 秒之后它保证读不到(惰性删除兜底),但内存是否立刻释放、什么时候真正删掉,取决于定期任务的节奏和内存压力。
第二个细节:maxmemory设为 0 表示不限制,这是默认值。很多本地开发环境就是这个状态,所以本地永远看不到淘汰行为,一上生产就出问题。本地调试淘汰相关逻辑时,务必显式设一个小值。
第三个:淘汰策略可以运行时修改,config set maxmemory-policy allkeys-lfu立即生效,不需要重启。但注意 LFU 的 counter 是对象创建时初始化的,从 LRU 切到 LFU 后,老对象的 counter 都是初始值,需要一段时间的访问才能积累出有效分布。所以策略切换后的一两天内,淘汰效果不代表稳态,观察期要给够。
我个人的几条使用习惯,供参考。缓存实例一律allkeys-lru+maxmemory-samples 10打底,这是最不容易出错的组合。有明显热点且做过访问分布分析的,才考虑换 LFU。maxmemory按物理内存的 70% 到 75% 设,留足 fork 和缓冲的余量。所有缓存 key 强制带 TTL 且加随机抖动,从代码层面杜绝"永久 key"和"批量同时过期"。大 key 一律开 lazyfree,并且在写入侧做大小校验。
还有个小技巧:给淘汰量加监控告警,evicted_keys的每分钟增量超过某个阈值就通知。这个指标比内存使用率更早反映问题——内存使用率贴顶是常态,但淘汰速率突然飙升往往意味着流量结构变了或者某个 key 在膨胀。我一般把阈值设在"常态峰值的 3 倍",触发后先看是哪个业务线的 key 在被淘汰,再决定是扩容还是改策略。
最后说一句关于命中率的观察:淘汰机制本质上是在"内存有限"和"数据都想留"之间做取舍,它的效果好不好,唯一标准是命中率而不是淘汰量。有些团队看到evicted_keys高就慌,急着加内存,其实如果命中率稳在 95% 以上,那淘汰只是正常工作。反过来,淘汰量不大但命中率掉,才是真正需要动手的信号。这个判断标准比任何参数配置都重要,也是我把这套机制摸透之后最大的收获。
附:常用命令速查
# 查看内存与淘汰概况 redis-cli info memory redis-cli info stats | grep -E "evicted_keys|expired_keys|keyspace_hits|keyspace_misses" # 运行时调整关键参数 redis-cli config set maxmemory 11gb redis-cli config set maxmemory-policy allkeys-lru redis-cli config set maxmemory-samples 10 redis-cli config set lazyfree-lazy-eviction yes # 扫描大 key 与过期分布 redis-cli --bigkeys redis-cli info keyspace这些命令我在处理线上问题时基本是条件反射级别地敲出来,建议也存一份到你自己的排查手册里。参数的具体数值要根据机器规格和业务形态调,上面给的数字是起点,不是标准答案。