Redis 主从架构下过期键到底谁来删?顺带聊聊大批量过期的雪崩防护

举报
yd_282927746 发表于 2026/10/28 08:49:54 2026/10/28
【摘要】 从库从不主动删除过期键:读取时判定过期返回 nil,真正的删除由主库用 DEL/UNLINK 传播。本文拆解惰性删除与定期采样机制,解释为什么过期键还能被 SCAN 到,并给出大量 key 同时过期时的 5 个解法与 4 个排查指标。

一、先问一个问题:从库上的键过期了吗?

很多人对 Redis 过期的理解停留在"到点就删"。实际上 Redis 从来没有一个精确的定时器去删除某个 key——它用的是惰性删除 + 定期删除的组合,而且这套机制只在主库上运行。

从库(replica)不主动清理过期键。它做的事只有两件:

  1. 收到读请求时,用本地的 TTL 判断这个键是否逻辑过期,过期就直接返回 nil(Redis 3.2 起的行为),但键在内存里还在;
  2. 等主库的删除命令同步过来,照单执行 DEL / UNLINK。

也就是说,从库上会出现一个反直觉的现象:

# 在从库上
127.0.0.1:6380> TTL session:abc
(integer) -2          # 已经过期
127.0.0.1:6380> GET session:abc
(nil)                 # 读到的是空
127.0.0.1:6380> DBSIZE
(integer) 1002        # 但键还占着内存,DBSIZE 里还算它

TTL 返回 -2、GET 返回 nil,但 DBSIZE 和 SCAN 还能看到这个键。这不是主从延迟 bug,是设计如此——从库如果自己删,就会和主库的数据版本不一致,因为两者的时钟、复制偏移、写入顺序都可能有差异。

二、主库到底怎么删

主库有两条路径:

惰性删除:每次访问一个 key,先走 expireIfNeeded()。过期了就删掉、返回 nil,同时把这个删除动作写进复制流。

定期删除:serverCron 每 100ms 触发一次 activeExpireCycle(),从"设置了 TTL 的字典"里随机采样一批键,删掉其中已过期的。这里有三个要点:

  • 是采样,不是全量遍历,所以未过期键多的实例里,过期键可能存活很久;
  • 单轮有时间上限(默认不超过 CPU 时间的 25%),扫不完就下一轮继续;
  • 正因为是采样,"过期时间到了"和"内存被释放"之间没有保证。

这就解释了一个常见疑惑:为什么有的 key 设了 60 秒 TTL,两小时后用 SCAN 还能扫到?因为主库既没被访问到它(惰性删除没触发),采样也没抽到它(定期删除没覆盖)。

三、复制流里到底传了什么

主库删除过期键后,会以普通写命令的形式传播:

DEL session:abc

Redis 4.0 之后,如果开了 lazyfree-lazy-expire yes,传播的是:

UNLINK session:abc

UNLINK 只把键从字典里摘掉,实际内存回收交给后台线程,避免大 key 删除阻塞主线程。从库收到 UNLINK 也一样做异步释放。

还有一个细节值得知道:主库不会把"过期时间点"单独同步过去——过期时间是通过 PEXPIREAT 这类命令在写入时就带过去的,从库自己也有这个绝对时间戳,所以从库能自行判断"逻辑上过期了",只是不会主动删。

四、什么时候会读到"过期了但还在"的值

三种典型场景:

场景 表现 原因
用 RANDOMKEY / SCAN / DBSIZE 做统计 数量和实际有效数据不符 过期键还在内存里
主库采样没扫到、也没人访问 从库能把旧值读出来(早期版本) 现在版本读时会判过期返回 nil,但 3.2 之前会返回旧值
用 KEYS * 做数据导出 导出到已过期的键 同上

结论很直接:不要把 Redis 的 TTL 当成业务上的强保证。如果业务要求"优惠券到期绝对不能再用",那就必须在应用层用 expireAt 字段做二次校验,Redis 的 TTL 只当"大概率清理"的手段。

五、大量 key 同时过期怎么办

这是压测和线上都常见的坑:早上 9 点整一批活动缓存同时到期,主库瞬间要处理大量 DEL 和回源请求,表现为延迟毛刺、CPU 飙高、数据库被打穿。五个可落地的解法:

1. TTL 加随机抖动

写入时不要给固定过期时间:

import random
ttl = 3600 + random.randint(0, 600)   # 1 小时 ±10 分钟
redis.setex(key, ttl, value)

把同一批数据的过期时间打散到 10 分钟窗口里,雪崩就变成了缓坡。

2. 热点 key 永不过期

对 Top N 热点数据,不设 TTL,靠后台任务定时刷新值 + 版本号。读路径永远命中,不会出现"同时失效同时回源"。

3. 逻辑过期 + 异步重建

值里带一个 expireAt 字段,读到时判断:

  • 没过期 → 直接返回;
  • 逻辑过期 → 先返回旧值,同时投递一个异步刷新任务(或用互斥锁 + 双重检查,只放一个请求去回源)。

这样请求永远不会被"卡住等回源",代价是可能短暂读到旧数据。对资讯、榜单这类场景完全可接受。

4. 开启异步删除

lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes

大 key(比如几十万元素的 Hash)过期时,DEL 是同步释放内存,能卡住主线程几百毫秒;UNLINK 把释放丢给后台线程,主线程只做摘链。

5. 分批写入 / 分批下线

批量导入 10 万条数据时,别一次性写同 TTL 的键。按 hash 分片或按时间窗分批,每批之间 sleep 一小会儿,让过期时间天然错开。

六、怎么确认是不是过期风暴

别猜,看指标:

redis-cli INFO stats | grep expired_keys     # 过期键速率,突增就是有集中过期
redis-cli INFO commandstats | grep -E 'cmdstat_(del|unlink)'
redis-cli SLOWLOG GET 10                     # 是不是删除操作变慢了
redis-cli LATENCY MONITOR on                 # 看主线程有没有被卡
redis-cli INFO replication | grep -E 'offset|lag'

判断顺序建议:先看 expired_keys 速率是否和业务高峰对齐,再看 SLOWLOG 里有没有 del/unlink 的长耗时,最后用 LATENCY MONITOR 确认是否阻塞主线程。如果复制偏移差值同时在扩大,说明删除风暴已经把复制链路拖慢了。

在华为云 DCS 上这套逻辑完全一致(DCS Redis 主从版/集群版都是标准复制模型),额外可以用控制台的慢查询分析、热 key 分析和实例监控里的"过期键"指标来定位。真要清大批量键,用 SCAN 游标 + UNLINK 分批删,别用 KEYS,也别在业务高峰 FLUSHDB。

七、小结

  • 过期是主库的职责:惰性删除 + 定期采样,两条腿走路,都不保证及时。
  • 从库只做两件事:读时判定过期返回 nil、被动执行主库传来的 DEL/UNLINK。
  • TTL 不是业务强一致手段,关键逻辑要有应用层兜底。
  • 防雪崩靠设计:TTL 抖散、热点不过期、逻辑过期 + 异步重建、异步删除、分批写入。
  • 排查靠指标:expired_keys 速率 → SLOWLOG → LATENCY MONITOR → 复制偏移。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。