根本原因是redis 3.2之前从库不判断也不删除过期key,主库未及时触发del命令同步时,从库仍返回已过期数据;升级至3.2+可解决,因新版本从库读取时会主动检查并返回空值。

从节点读到过期数据,根本原因不是同步延迟,而是从库根本不删过期 key —— 它连判断都不做(3.2 之前)。
Redis 3.2 之前:从库对过期 key 完全“视而不见”
主库写入 SET foo bar + EXPIRE foo 10 后,过期逻辑只在主库触发:惰性删除或定期删除。但这些删除动作产生的 DEL foo 命令,是作为复制流的一部分发给从库的;如果主库还没来得及删,从库就只能原样存着这个已过期的 foo。
更关键的是:GET foo 发到从库时,旧版本(foo 是否过期,直接返回值 —— 即使它已在主库被删、或按时间戳早已过期。
常见错误现象:
- 缓存击穿后重设 key,但下游服务从从库读到空值或旧值,误判为“缓存未命中”反复回源
- 订单状态缓存设置 5 分钟过期,用户刷新页面却看到“已取消”状态持续十几分钟
Redis 3.2+ 的 lookupKeyRead 改动:从库也做“过期快照判断”
3.2 版本在 db.c 中修改了 lookupKeyRead 函数逻辑:每次读操作前,先查 key 的过期时间戳,再比对当前时间。若已过期,立即返回 NULL(即空响应),而不是返回原始 value。
注意这不是“主动删除”,只是“拒绝返回”。所以:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 从库内存里仍存着过期 key,
KEYS *或SCAN还能扫出来 没有额外 GC 开销,也不影响主从同步协议
- 客户端收到的是
nil(如 Redis CLI 显示(nil)),行为上等价于“key 不存在”
EXPIRE vs EXPIREAT:命令选错,3.2 也救不了
即使升级到 3.2+,用 EXPIRE 或 PEXPIRE 依然可能读到过期数据 —— 因为它们基于“相对时间”,主从执行时刻不同,导致从库计算出的过期时间比主库晚。
例如主库在 t=1000ms 执行 EXPIRE key 5000,key 应在 t=6000ms 过期;但从库在 t=1050ms 才收到并执行该命令,它会把过期时间算成 t=6050ms —— 多出 50ms 的窗口期。
正确做法:
- 统一改用
EXPIREAT/PEXPIREAT,传入绝对时间戳(如EXPIREAT key 1748152320) - 确保业务侧生成时间戳时使用 NTP 校准过的系统时间,避免主从时钟漂移放大误差
升级后仍需验证:别只看版本号
升级完成不代表问题消失。真实环境中容易忽略的点:
- 客户端连接的是否真是新版本从库?检查
INFO server中的redis_version字段,而非依赖部署文档 - 是否混用了多个 Redis 实例?某些中间件(如 Twemproxy、Codis)可能把请求路由到旧实例
- 从库是否启用了
slave-read-only yes(必须开启),否则可能被误写入干扰判断逻辑
最简验证命令:SET testkey "x" && EXPIRE testkey 1 && SLEEP 2 && GET testkey —— 在从库执行,预期返回 (nil),而非 "x"。










