news 2026/10/27 13:00:51

Redis过期删除与内存淘汰:机制拆解、策略选型与排查实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis过期删除与内存淘汰:机制拆解、策略选型与排查实战

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比主库高出好几个 G,dbsize却差不多,最后定位就是主库的定期删除还没覆盖到那批 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超过maxmemory,Redis 在执行命令前会调用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:key

3.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,让热点节点多分点内存。第二种做法运维上有点麻烦,但应急时很有效。我在一个偏斜比较严重的集群上试过按节点调maxmemory,evicted_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:1001

4.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 统计的内存是否贴近 maxmemory
info 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 GB,used_memory4.2 GB 已经接近 6 GB 的 70%……等等,70% 是相对物理内存算的,相对maxmemory已经 70% 且还在爬。这里就暴露一个常见误判:监控面板上的内存使用率如果按物理内存算,会掩盖maxmemory设置过低的问题。后来统一改成按used_memory / maxmemory展示。

第二步,看淘汰策略。是allkeys-random。随机淘汰意味着热 key 和冷 key 一视同仁,热 key 被踢的概率和它占的比例成正比。这就是命中率跌的直接原因。

第三步,确认数据特征。抽样看了几百个 key 的访问情况,明显是长尾分布,少数 key 承担了绝大部分访问。这种分布用 LFU 最合适。

最终处理:策略改成allkeys-lfu,maxmemory从 6 GB 提到 10 GB,maxmemory-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必须显式声明,禁止依赖默认值。这三条写进代码规范和部署检查清单,能挡掉一大半问题。

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

用Pandas+Plotly玩转平行坐标图:多维数据探索与聚类校验

1. 为什么我要把平行坐标图搬进 Pandas 的日常分析流程做数据分析的人大概都有过这种体验:手里一张十几列、几十万行的宽表,做完describe()之后盯着那一堆均值、分位数、标准差发呆,心里其实没底——数据到底长什么样?各维度之间有…

作者头像 李华
网站建设 2026/10/27 12:59:59

X-Plane 11 UDP数据采集实战:Python实现高精度飞行黑匣子

简介:StableApproach 是一款专为 X-Plane 11 飞行模拟器设计的飞行数据监控(FDM)增强工具,面向飞行模拟爱好者、航校学员及进阶飞行员,聚焦稳定进近阶段的关键参数分析与操作评估,助力用户通过真实数据复盘…

作者头像 李华
网站建设 2026/10/27 12:59:59

Qwen3-30B异步训练优化:混合并行与梯度压缩实战

1. 项目背景与核心挑战 Qwen3-30B-A3B作为当前主流的大规模预训练语言模型之一,在自然语言处理、内容生成等场景展现出强大能力。但在实际工业级部署中,我们发现其异步训练场景下的性能表现与理论计算能力存在显著差距——在8卡A100集群上,异…

作者头像 李华
网站建设 2026/10/27 12:59:55

Myolotrain平台v4.x升级:分布式训练与MLOps优化实践

1. Myolotrain平台升级背景与技术动因 作为一款专业AI训练平台,Myolotrain的每次版本迭代都直接关系到数千家企业级用户的模型研发效率。本次从v2.x到v4.x的跨代升级,主要源于三大技术驱动力: 首先是分布式训练框架的重构。旧版本基于Tensor…

作者头像 李华
网站建设 2026/10/27 12:58:05

WorkBuddy+Obsidian:公式教材自动化生产与智能出题工作流

1. 培训教材生产的真实困境与破局思路做培训讲师这行十几年,最磨人的从来不是站在讲台上那几个小时,而是台下那些看不见的重复劳动。尤其是理科方向的培训,公式教材的编写和题库的维护,几乎占据了我一半以上的备课时间。一份力学或…

作者头像 李华
网站建设 2026/10/27 12:58:04

OpenMontage:面向视频生产的智能Agent协同操作系统

1. OpenMontage不是另一个AI视频工具,而是一套面向专业视频工作流的智能代理协同系统 OpenMontage这个名字乍一听容易让人联想到开源版的Adobe Premiere或DaVinci Resolve——毕竟“Montage”在影视行业里专指剪辑本身,是镜头语言组织的核心动作。但实际…

作者头像 李华