1. 先分清两件事过期删除和内存淘汰不是一码事这一讲的标题起得很实在因为很多人第一次接触 Redis 淘汰机制的时候脑子里是糊的明明设置了expire为什么内存还是涨明明内存还没满为什么 key 就不见了我见过太多排查过程最后发现是把“过期删除”和“内存淘汰”当成了同一套机制在理解。这两件事在 Redis 内部是两条独立的代码路径触发条件不同、执行时机不同、出问题时表现出来的症状也完全不同混在一起谈排查方向一定跑偏。先把概念摆清楚。过期删除处理的是“这个 key 已经到点了该走了”它关心的是时间触发源是 key 自身携带的 TTL内存淘汰处理的是“内存要撑爆了得踢人”它关心的是maxmemory这条红线触发源是写入命令导致的内存增长。一个 key 完全可能 TTL 还有三天但因为内存紧张被 LRU 策略提前踢出去反过来一个 key 早就过期了但因为一直没人访问、定期删除也没采样到它它就那么占着内存躺着直到某次内存淘汰顺手把它清掉。我印象最深的一次线上问题是某业务系统的缓存集群内存使用率长期在 85% 上下晃运维同学一直在加内存加了两次还是涨。后来翻了info stats才发现evicted_keys每天几十万地涨expired_keys反而很平。这说明什么说明绝大多数 key 根本不是自然过期走的是被淘汰踢走的。再往下查业务侧给所有 key 都设了 24 小时 TTL但实际数据热度是“写入后两小时内被访问之后基本没人碰”也就是说大批 key 在过期之前就已经冷掉了白白占着内存逼着 Redis 去淘汰真正还热的数据。问题根子不在 Redis 配置在 TTL 设计。所以这一讲我想讲透的东西很明确把两条路径各自拆开看清楚再讲清楚它们怎么配合、怎么互相干扰、怎么通过配置和监控把它们调到符合业务预期的状态。适合谁看手上管着 Redis 实例、被内存告警和命中率下跌折腾过的同学也适合刚学分布式缓存、只知道maxmemory-policy有八个选项但不知道选哪个的同学。下面这些内容里参数、命令、观测方法都是可以直接抄去用的。2. 过期删除惰性删除加定期删除两条腿走路2.1 惰性删除干了什么代价是什么惰性删除的逻辑非常朴素当客户端访问某个 key 时Redis 先在过期字典里查一下这个 key 有没有设置过期时间如果设了并且当前时间已经超过它就当场把它删掉然后返回空。这个判断发生在lookupKey系列函数里读命令、写命令都会走。用生活化的比喻这就像你家冰箱里的牛奶你不去拿它就没人管你伸手去拿的时候才发现过期了顺手扔掉。这个设计的最大好处是不额外消耗 CPU只有真正被访问的 key 才会产生检查成本冷数据完全不占用资源。但代价也很明显如果一个 key 过期之后永远没人再访问它就会一直留在内存里形成所谓的“僵尸 key”。想象一下一个活动页面缓存了几百万个用户维度的 key活动结束后这些 key 设置了 3 天过期但再也没有请求打过来那这堆内存就得等到三天后被别的机制清掉或者干脆撑到内存淘汰。纯靠惰性删除内存回收是完全被动的。所以 Redis 不可能只靠惰性删除必须有第二个机制兜底也就是定期删除。这两个是配合关系不是二选一。2.2 定期删除的采样节奏与时间预算定期删除挂在 Redis 的周期任务上大约每 100 毫秒跑一次取决于hz配置默认hz 10。它不是一次性把所有过期 key 扫一遍——那样在大实例上会直接把主线程卡死——而是采用随机采样加比例判断的做法从过期字典里随机取一批 key检查并删除其中已过期的如果这批样本里过期 key 的占比超过 25%说明过期 key 密度还很高就再来一轮直到占比降下来或者用完这一轮的时间预算。时间预算这块有个参数值得单独说active-expire-effort取值 1 到 10默认 1。它的本质是控制定期删除愿意花多少 CPU。默认值 1 的时候慢速循环占用的 CPU 时间大约在个位数百分比到四分之一之间浮动快速循环只给毫秒级的时间片。如果你的业务里大量 key 集中过期比如整点批量刷新缓存默认配置下清理速度可能跟不上写入速度表现出来就是内存曲线在整点后有一段“该降没降”的平缓期。把active-expire-effort调到 3 到 5清理会积极很多代价是那段时间 CPU 使用率上升。# 查看当前的过期清理努力程度 redis-cli config get active-expire-effort # 适度提高注意观察 CPU redis-cli config set active-expire-effort 3注意active-expire-effort调高不是没有代价的尤其是在 CPU 已经打满或者实例规格很小的场景下贸然调到 10 可能让主线程在过期清理上占用过多时间片反而拖慢正常请求。建议一次调一档观察一轮再决定。还有一个更常见的坑大量 key 在同一秒过期。比如所有缓存统一写成expire key 3600那么每小时整点会有一批 key 同时进入过期状态定期删除的采样压力瞬间抬高惰性删除又跟不上就会看到 CPU 抖动和内存阶梯式下降。解决办法很土但有效——给 TTL 加随机抖动# 3600 秒基础上加 0 到 300 秒的随机偏移 redis-cli set cache:user:1001 data EX $((3600 RANDOM % 300))在客户端代码里做同样的事把 TTL 变成base random(0, base*0.1)过期压力就被摊平了。这个改动成本极低收益却很明显我在几个实例上做过对比整点的 CPU 尖刺基本消失。2.3 主从和持久化场景下的过期语义这块是很多人没想明白的地方。从库不会主动删除过期 key。从库上读到一个已经过期的 key会返回空给客户端但它自己不会动手删而是等待主库删除后同步过来的DEL命令。为什么要这样设计因为主从数据必须保持一致如果从库自作主张删了主库那边因为某些原因还没删比如主库认为这个 key 还被引用着主从数据就对不上了。这个设计带来一个现实问题如果主从同步出现延迟或者主库那边因为采样没覆盖到而迟迟不删从库上这些“逻辑过期但物理还在”的 key 会持续占着内存。以前排查过一个现象从库的used_memory比主库高出好几个 Gdbsize却差不多最后定位就是主库的定期删除还没覆盖到那批 key从库只能干等着。所以监控上一定要把主从两边的used_memory和expired_keys分开看别只看主库。持久化这边也有讲究。主库生成 RDB 时会过滤掉已经过期的 key所以 RDB 文件里不会有僵尸数据但如果是从库生成 RDB为了保持和主库一致过期的 key 默认是保留的由rdb-save-incremental-fsync等配置和内部逻辑决定。AOF 这边主库删除一个过期 key 之后会往 AOF 追加一条DEL新版本是UNLINK从库或重启恢复时执行这条命令完成删除。理解这个链路你就明白为什么“主库内存正常、从库内存偏高”是个常见现象而不是 bug。3. 内存淘汰策略八个选项到底怎么选3.1 全量策略对照与选型逻辑当used_memory超过maxmemoryRedis 在执行命令前会调用freeMemoryIfNeeded尝试释放空间。放不出来怎么办看策略。八个策略拆开其实是两个维度候选范围所有 key 还是只挑有 TTL 的 key和挑选算法LRU、LFU、随机、TTL 最短。策略候选范围挑选逻辑典型适用场景主要风险noeviction不淘汰达到上限后写命令报错数据不可丢、需要强一致的场景写入直接失败业务报错allkeys-lru所有 key近似最近最少使用通用缓存热点集中冷热突变时可能误伤allkeys-lfu所有 key近似最少使用频次有明显长尾、访问频次差异大新 key 冷启动易被淘汰allkeys-random所有 key随机访问分布极均匀或测试环境可能踢掉热点数据volatile-lru仅带 TTL 的 key近似 LRU缓存与持久数据混存无 TTL 的 key 永不被淘汰volatile-lfu仅带 TTL 的 key近似 LFU同上且频次差异大同上volatile-random仅带 TTL 的 key随机少见的特殊场景同上volatile-ttl仅带 TTL 的 key剩余 TTL 最短优先短期凭证、验证码类数据长期缓存反而活得久选型上我的建议很直接纯缓存用途用allkeys-lru或allkeys-lfu同一个实例里既有缓存又有关键业务数据用volatile-*系列并且确保所有缓存 key 都带 TTL。最怕的是那种“一半 key 有 TTL 一半没有”的混合用法又选了volatile-lru结果内存满了之后发现能淘汰的只有那批带 TTL 的全踢完了还是不够最后还是报 OOM。allkeys-lru和allkeys-lfu之间怎么选我通常这么判断如果业务访问呈现“少数 key 被反复访问其余几乎不访问”的强热点特征LFU 更稳因为它按频次判断不会因为某个热点 key 恰好在淘汰那一刻没被访问就被踢掉如果访问是“最近用过的还会继续用”的时间局部性特征比如信息流翻页、会话数据LRU 更合适。LFU 的短板是新 key 冷启动刚写入的 key 频次计数很低在一堆老热点里排名靠后很容易刚写进去就被淘汰形成“写了就没”的抖动。后面的章节会讲 Redis 用什么机制缓解这个问题。3.2 noeviction 的真实含义与踩坑点noeviction是默认值很多人装完 Redis 就直接跑业务从来没改过。这个策略的行为是内存达到maxmemory后所有会增加内存的写命令直接返回错误报错信息长这样(error) OOM command not allowed when used memory maxmemory.读命令和删除命令还能正常执行。这个设计本身没问题问题在于很多业务方的客户端没做OOM错误的容错一出现就是大面积异常。我见过几次事故都是这个原因缓存集群没设淘汰策略某天流量高峰把内存顶到上限所有写缓存的请求全部报错而业务代码里set失败没有兜底直接把异常抛给了上游。注意如果你确实要用noeviction比如用 Redis 存不能丢的队列或配置必须做到两件事一是写命令的异常处理要到位二是maxmemory要留足余量并配置内存告警别等撑满了才发现。我更推荐的做法是缓存用途一律用allkeys-*别留恋默认值。顺带说一个容易忽略的配置项lazyfree-lazy-eviction。默认是no意思是淘汰 key 时同步释放内存。如果被淘汰的 key 特别大比如一个几十 MB 的 hash同步释放会造成明显的延迟毛刺。把它改成yes淘汰和过期删除的内存释放走后台线程主线程压力小很多。同理还有lazyfree-lazy-expire处理大 key 过期时的卡顿。redis-cli config set lazyfree-lazy-eviction yes redis-cli config set lazyfree-lazy-expire yes # 直接删除大 key 时也建议 redis-cli unlink big:hash:key3.3 maxmemory 到底该设多少一个可复现的计算过程maxmemory不是“物理内存减去系统占用”这么简单。Redis 判断是否超限用的是自身统计的used_memory这个值不包含内存碎片带来的额外开销也不包含 fork 子进程时的写时复制copy-on-write内存。所以如果你把maxmemory设成物理内存的 90%一次 RDB 落盘触发的 fork 加上 COW很容易被系统 OOM killer 干掉。我的经验公式是这样的容器或机器可用内存为M如果开启了 RDB 或 AOF rewrite按maxmemory M * 0.55 ~ 0.65设如果没有持久化、纯缓存可以放宽到M * 0.75。剩下的空间留给三块fork 期间 COW 的页拷贝最坏情况和写入量正相关、客户端输出缓冲区、AOF 重写缓冲区。举个具体例子一台 16 GB 的机器跑单个 Redis 实例开启了 RDB 每小时一次、AOF 每秒刷盘。按 0.6 计算maxmemory设 9.6 GB 左右取整 9 GB。留出的 7 GB 里最坏情况 RDB fork 时写入量大COW 可能吃掉几 GB加上客户端缓冲和碎片勉强够。如果这台机器上还跑了别的服务那就得继续往下压。# 动态调整无需重启 redis-cli config set maxmemory 9gb redis-cli config set maxmemory-policy allkeys-lru redis-cli config rewrite # 把当前配置写回配置文件避免重启丢失注意config set的调整在重启后就没了一定要跟一句config rewrite。但如果配置文件是挂载的只读文件或者由配置中心管理config rewrite会失败这种场景就得走配置中心改。3.4 集群模式下每个节点各算各的分片集群里每个节点独立判断自己的maxmemory没有一个全局协调者。这意味着如果 key 在分片之间分布不均会出现“某些节点已经拼命淘汰、另一些节点还很空”的情况。表现出来就是整体内存使用率只有 60%但evicted_keys一直在涨。这时候要查的是分片是否倾斜cluster slots看槽位分布、info keyspace看各节点 key 数量、redis-cli --cluster check看是否有大 key 集中在个别节点。办法有两个方向一是用 hash tag 或者改造 key 结构让分布更均匀二是给每个节点设不同的maxmemory让热点节点多分点内存。第二种做法运维上有点麻烦但应急时很有效。我在一个偏斜比较严重的集群上试过按节点调maxmemoryevicted_keys立刻降了一个数量级。4. LRU 和 LFU 的近似实现为什么不准反而更好4.1 redisObject 里那 24 位这是我觉得 Redis 设计里最精妙的地方之一。严格实现一个 LRU需要维护一条双向链表每次访问都把节点移到链表头内存开销是每个 key 两个额外指针操作也有开销。Redis 没这么干它只在每个对象的redisObject结构里挤出了24 位用来记录访问信息。在 LRU 模式下这 24 位存的是一个秒级精度的时钟戳也就是这个对象最后一次被访问的大致时间。取当前时间、和对象里存的时间一比就得到了空闲时长。24 位存秒级时间戳大概能表示 194 天左右超过就回绕所以 Redis 里有个说法是“LRU 时钟精度足够用长空闲的 key 判断可能不准但它反正都要被淘汰不准也无所谓”。在 LFU 模式下同样 24 位被拆成两块高 16 位存上次访问时间分钟级低 8 位存访问频次计数器。8 位最多到 255所以 LFU 计数有上限不会无限涨。这个拆分意味着 LFU 模式下的时间精度是分钟级OBJECT IDLETIME这类命令在 LFU 策略下是不可用的会报错要查频次得用OBJECT FREQ。# LRU 策略下查看 key 的空闲秒数 redis-cli object idletime cache:user:1001 # LFU 策略下查看访问频次计数器 redis-cli object freq cache:user:10014.2 采样淘汰maxmemory-samples 的精度与代价Redis 淘汰时并不遍历所有 key 去找最该踢的那个而是随机采样若干个 key从中挑最差的那个。采样数量由maxmemory-samples控制默认 5。这个数字直接决定了近似算法的精度采样越多越接近真实的 LRU/LFU但每次淘汰的计算开销也越大。官方文档里有一组对比图样本数从 5 提到 10淘汰效果用未被淘汰的热数据比例衡量提升很明显从 10 提到 20提升就变缓了。我的实测感受是大多数业务 5 就够用命中率敏感的场景调到 10 性价比最高超过 10 之后收益递减反而在淘汰高频发生时增加延迟。redis-cli config set maxmemory-samples 10有个细节值得说明采样是从整个 db 的字典里随机取的如果实例里 key 数量很大千万级每次采样其实就是在做一次随机探测。所以maxmemory-samples调得过大在淘汰频繁的场景下主线程花在采样上的时间会累积成可观的延迟。曾经有个实例maxmemory-samples被设成 64淘汰高峰时 p99 延迟飙到 30ms 以上改回 10 之后立刻平稳。4.3 淘汰池新 key 为什么不会被立刻踢掉单纯“采样取最差”有个致命缺陷刚写入的 key 没被访问过空闲时间短、频次低一旦被采样到就会被判为“最差”导致刚写进去的数据立刻被淘汰形成抖动。Redis 用一个叫eviction pool的机制来缓解。具体做法是维护一个固定大小的候选池内部是 16 个槽位每次淘汰时采样出来的 key 会先放进池子里池子按空闲时长或者频次排序。然后从池子里挑最该淘汰的那个踢掉。池子里的候选会跨多轮保留这意味着一个 key 进入池子后如果后续被访问了它的空闲时长会刷新排名下降就不会被淘汰而新写入的 key 会进池但它需要“熬过”一段时间等其他更老的 key 被淘汰出去。这个设计有点像排队叫号新来的先取号但叫号顺序按等待时长排不会因为刚来就被赶走。理解了这一层你就明白为什么 LFU 下新 key 的冷启动问题被缓解了也明白为什么maxmemory-samples不需要设得特别大——池化机制本身已经在弥补采样的不足。4.4 LFU 的计数增长与衰减两个参数掰开说LFU 的计数器不是每次访问就加 1而是概率性增长。计数越大增长越难。公式大意是计数器为c每次访问时以1 / ((c - 初始值) * log_factor 1)的概率加一。log_factor就是lfu-log-factor默认 10。这个设计的好处是1 次访问和 10 次访问、100 次访问和 1000 次访问计数器能拉开明显差距同时上限 255 不容易被轻易顶满。另一个参数是lfu-decay-time默认 1单位是分钟。意思是如果某个 key 超过 N 分钟没有被访问计数器就按时间差衰减。这就解决了“过去很热、现在冷了”的问题——老热点不会因为历史计数高就永远霸着内存。参数默认值作用调大的效果调小的效果maxmemory-samples5采样数量淘汰更精确CPU 开销上升更省 CPU精度下降lfu-log-factor10计数增长难度更难达到高频区分度更高计数涨得快容易饱和lfu-decay-time1频次衰减周期分钟衰减慢历史权重高衰减快更看重近期访问调整这两个参数要有明确目标。如果业务发现热点 key 被误杀先看OBJECT FREQ的值如果热点 key 的 freq 只有几十说明计数涨得太慢可以考虑把lfu-log-factor调小如果发现自己明明已经降流的 key 还占着内存不走把lfu-decay-time调小让衰减更快。5. 淘汰链路走一遍从一条命令到一次驱逐5.1 命令执行前的检查顺序完整链路大致是这样客户端发来的命令被解析后进入processCommand在这里 Redis 会先做一些通用检查然后调用freeMemoryIfNeededAndSafe判断当前used_memory是否超过maxmemory是否处于需要阻塞的状态比如正在做 AOF 刷盘且策略是noeviction的部分场景。如果确实超了就进入freeMemoryIfNeeded执行淘汰直到内存降到红线以下或者再也淘汰不出东西。淘汰失败命令直接返回 OOM 错误。这个顺序很重要淘汰是发生在命令执行之前的。所以你在客户端看到的错误往往不是因为这条命令本身写入了太多数据而是它到来时内存已经是满的。这也解释了为什么 OOM 错误经常集中在流量高峰的某个时间点爆发而不是均匀出现。还有一个链路细节key 的过期判断发生在查找 key 的时候也就是expireIfNeeded里。如果这个 key 被判定过期主库会删掉它并传播DEL命令本身按“key 不存在”处理。写命令不会因为覆盖一个过期 key 而触发特殊逻辑正常写入即可。5.2 手把手复现一次驱逐看指标变化理论说完动手跑一遍最直观。准备一个测试实例把内存限死制造淘汰# 起一个测试实例仅本地验证用 redis-server --port 6380 --maxmemory 64mb --maxmemory-policy allkeys-lru # 另开终端写入大量带过期时间的 key redis-cli -p 6380 config set maxmemory 64mb redis-cli -p 6380 config set maxmemory-policy allkeys-lru redis-benchmark -p 6380 -n 200000 -r 200000 -d 512 -t set跑完之后看指标redis-cli -p 6380 info memory redis-cli -p 6380 info stats | grep -E evicted_keys|expired_keys|keyspace redis-cli -p 6380 info keyspace你会看到evicted_keys明显增长used_memory稳定在maxmemory附近不再上涨db0的 key 数量远小于你写入的总数。再把策略换成volatile-lru并写入一批不带 TTL 的 key 做对照你会发现不带 TTL 的 key 一个都不会被淘汰evicted_keys只挑带 TTL 的踢。这个对照实验我建议每个刚接触这块的人都做一遍比看十篇文章都管用。还可以验证 LFU 的行为redis-cli -p 6380 config set maxmemory-policy allkeys-lfu redis-cli -p 6380 set hot:key value # 反复访问制造高频 for i in $(seq 1 500); do redis-cli -p 6380 get hot:key /dev/null; done redis-cli -p 6380 object freq hot:key看到freq值涨到几十甚至上百说明计数在起作用。然后把内存压到很紧持续写入新 key观察hot:key是否还在——大概率还在这就是 LFU 保护热点的效果。5.3 监控指标怎么配看什么我把排查时最常用的指标列一下建议全部纳入监控面板指标来源字段含义关注点info memoryused_memoryRedis 统计的内存是否贴近 maxmemoryinfo memoryused_memory_rss操作系统看到的物理内存与 used_memory 的比值info memorymem_fragmentation_ratio碎片率长期大于 1.5 要考虑整理info memorymaxmemory配置上限与 used_memory 比较info statsevicted_keys累计淘汰数持续增长说明内存不足info statsexpired_keys累计过期删除数与写入量对比看清理效率info statskeyspace_hits/misses命中与未命中命中率的原始数据info commandstats各命令调用次数命令分布找异常写入源命中率要自己算hits / (hits misses)。单独看misses增长没意义因为流量涨了 misses 也会涨。我习惯把淘汰率和命中率放在同一张图上看如果淘汰量上升的同时命中率下降基本可以断定是内存不够导致热数据被踢如果淘汰量上升但命中率没变说明被淘汰的确实是冷数据暂时可以接受但要规划扩容。5.4 大 key 和批量写入对淘汰的影响一个大 hash 占几百 MB被淘汰时哪怕用了lazyfree释放过程中也要消耗资源而且它在单个 key 的维度上“分量太重”近似 LRU 采样很容易把它选中——因为按 key 计数它和其他小 key 权重一样但它释放出来的空间是小 key 的成千上万倍。这看起来是好事实际上会打乱淘汰节奏踢掉一个大 key 之后内存一下子降到红线以下淘汰停止一堆该走的小 key 又被留下等内存重新涨上来再循环一次内存曲线呈锯齿状。批量写入同理。用pipeline一次塞十万条这十万条几乎同时到达内存瞬间顶到上限淘汰被触发但因为淘汰是逐轮采样进行的可能来不及最终部分命令报 OOM。稳妥做法是控制单次批量的大小比如每批 500 到 1000 条中间留出短暂间隔给淘汰一点喘息时间。6. 常见问题与排查实录6.1 问题速查表现象可能原因排查动作处理方式写入报 OOM 错误策略为 noeviction 或候选 key 耗尽查 maxmemory-policy、info memory改策略或扩容evicted_keys 持续增长内存不足对比 used_memory 与 maxmemory扩容或优化 TTL命中率突然下跌热 key 被淘汰查淘汰策略、maxmemory-samples换 LFU、加样本数内存降不下来无 TTL 的 key 占大头抽样 SCAN 统计 TTL 覆盖率补齐 TTL 或迁移数据从库内存远高于主库从库被动等待删除对比主从 used_memory、dbsize查主库主从延迟CPU 周期性抖动大量 key 集中过期看 expired_keys 的时间分布TTL 加随机抖动淘汰时延迟毛刺大 key 同步释放查慢日志、大 key 扫描开 lazyfree、拆分大 key整体内存不高但一直淘汰集群分片倾斜cluster slots、各节点 keyspace重分布或调各节点上限6.2 一次命中率下跌的完整排查过程说个实际案例。某段时间一个缓存实例的命中率从 97% 掉到 88%同时接口 p99 从 12ms 涨到 40ms但内存使用率只有 70%看起来不像内存问题。排查按这个顺序走第一步看evicted_keys。发现它确实在涨每天几万。内存才 70% 为什么淘汰查maxmemory配置发现是 6 GB而物理内存是 16 GBused_memory4.2 GB 已经接近 6 GB 的 70%……等等70% 是相对物理内存算的相对maxmemory已经 70% 且还在爬。这里就暴露一个常见误判监控面板上的内存使用率如果按物理内存算会掩盖maxmemory设置过低的问题。后来统一改成按used_memory / maxmemory展示。第二步看淘汰策略。是allkeys-random。随机淘汰意味着热 key 和冷 key 一视同仁热 key 被踢的概率和它占的比例成正比。这就是命中率跌的直接原因。第三步确认数据特征。抽样看了几百个 key 的访问情况明显是长尾分布少数 key 承担了绝大部分访问。这种分布用 LFU 最合适。最终处理策略改成allkeys-lfumaxmemory从 6 GB 提到 10 GBmaxmemory-samples从 5 提到 10重启后命中率回到 96% 以上p99 降到 15ms 以内。事后复盘根子是当初部署时随手用了默认的低内存配置和随机策略没人复核。6.3 几个不写在文档里的坑第一used_memory和系统看到的内存不是一回事。客户端缓冲区、复制缓冲区、AOF 缓冲区这些都算在used_memory里但内存碎片不算。所以经常出现系统RSS15 GB、used_memory10 GB、maxmemory12 GB 的组合Redis 自己觉得还很宽松操作系统已经快撑不住了。这种时候要做的是碎片整理或者干脆迁移重启而不是继续调maxmemory。# 开启主动碎片整理Redis 4.0 redis-cli config set activedefrag yes redis-cli config set active-defrag-ignore-bytes 100mb redis-cli config set active-defrag-threshold-lower 10第二别用keys *去统计 TTL 覆盖率。主线程会直接卡住线上环境尤其危险。用scan游标分批扫每批 100 到 500 个中间可以 sleep 一下。第三del大 key 会阻塞。一个几百万元素的 zset 用del删主线程可能卡几百毫秒。改用unlink让后台线程慢慢释放。这个改动在我们一个实例上把删除大 key 造成的毛刺从 200ms 降到了个位数毫秒。第四修改策略后不要只看当前指标。从allkeys-lru切到allkeys-lfu之后所有 key 的频次计数是从零开始重建的头几个小时的淘汰判断是不准的可能出现短暂命中率波动。至少观察一个完整的业务周期再下结论。第五maxmemory不是越小越安全。有人为了“防止内存打爆”把maxmemory设得很保守结果天天淘汰、命中率上不去数据库压力反而更大。缓存的意义是挡住流量为了省几 GB 内存把缓存打成半残账算不过来。6.4 关于过期和淘汰的一点延伸思考我在实际使用中发现绝大多数“Redis 淘汰机制出问题”的案例最终根因都不在 Redis 配置本身而在key 的设计和 TTL 的规划。同样的内存和同样的策略把 TTL 从“统一 24 小时”改成“按数据热度分级设置热数据 2 小时、温数据 12 小时、冷数据 3 天”再把批量过期的时间点打散淘汰量和命中率都能明显改善。配置是最后的兜底手段数据模型才是第一道防线。如果这套东西想在项目里落地成规范我建议至少定三条所有缓存 key 必须显式设置 TTL禁止裸写TTL 必须带随机抖动禁止整点批量过期maxmemory-policy必须显式声明禁止依赖默认值。这三条写进代码规范和部署检查清单能挡掉一大半问题。