“永不过期”策略实质是Redis key物理永存、逻辑过期时间嵌入value中;需用SETNX抢锁、双重检查、异步更新,并必须搭配主动刷新机制防脏数据。

Redis内存淘汰策略误配会直接放大缓存击穿风险
不是所有“缓存未命中”都等于业务逻辑问题——如果 maxmemory-policy 被设为 allkeys-lru 或 volatile-lru,而你又给热点 key 设置了 TTL(哪怕很长),那它就可能在压力下被提前逐出。此时“永不过期”策略实际已失效,击穿照常发生。
典型现象是:监控显示 evicted_keys 持续上涨,但业务日志里大量 cache miss 出现在非过期时间点;数据库慢查询集中在几个固定 key 上,且这些 key 在 Redis 中查不到(EXISTS hot_key 返回 0)。
- 必须确认当前策略是否与“永不过期”意图冲突:
noeviction是唯一真正兼容该策略的选项;allkeys-lfu虽倾向保留高频 key,但 LFU 计数器有衰减周期,冷启或流量突变时仍可能误删 - 若必须用
volatile-lru(例如历史遗留配置),那就不能依赖“不设 TTL”,而要显式用PERSIST hot_key清除其过期时间,否则SET hot_key value会继承默认 TTL - 线上切策略前务必压测:改完
maxmemory-policy后,用redis-cli --scan --pattern "hot_*" | xargs -n 100 redis-cli ttl批量验证热点 key 的 TTL 是否真为 -1
“永不过期”不等于“不更新”,后台刷新线程必须带版本控制
把 user:1001 设成永不过期后,没人主动更新,它就真的永远 stale 了。一线大厂真实做法是:用一个独立线程池 + 带版本号的异步刷新,而不是靠定时任务无差别重刷。
- 每次写 DB 成功后,往 Redis 写一个
hot_key_version,值为时间戳或自增 ID;刷新线程只加载version高于本地缓存的记录 - 避免全量刷新拖垮 DB:对
hot_key加HASH结构,比如HSET user:1001 info "{...}" version 1672531200,刷新时用HGET user:1001 version判断是否需要更新 - 刷新失败要有降级:比如连续 3 次超时,自动 fallback 到“逻辑过期”模式,value 里塞
{"data":{...},"expire_at":1672534800},后续读请求自行判断
Java 里用 RedisTemplate 实现安全的永不过期写入
直接调 opsForValue().set("hot_key", data) 很危险——如果 Spring 配置了默认 timeUnit 和 timeout,它会偷偷加上过期时间。必须显式切断这个链路。
- 用
boundValueOps("hot_key").set(data),它绕过全局 timeout 配置,等价于原生命令SET hot_key data - 若需原子性更新+版本校验,组合
EVAL脚本:redisTemplate.execute(new DefaultRedisScript("if redis.call('hget', KEYS[1], 'version') >= ARGV[1] then return 0 else redis.call('hset', KEYS[1], 'data', ARGV[2], 'version', ARGV[1]) return 1 end", Long.class), Arrays.asList("hot_key"), "1672531200", json) - 禁止在刷新线程里用
setIfAbsent:它本质是SETNX,不带过期时间,但若 key 已存在且没过期,操作直接失败,导致刷新中断
为什么“物理永不过期 + 后台刷新”比“逻辑过期”更难落地
表面看逻辑过期只是多存个时间戳,但生产环境里它暴露的是整个数据读写链路的治理能力缺陷。
- 所有写入口必须走统一 SDK 封装,否则任意一个直连
redis-cli SET hot_key raw_json就会让逻辑过期字段丢失,整个策略崩塌 - 缓存序列化格式一旦变更(如从 JSON 改 Protobuf),旧逻辑过期结构无法解析,容易静默失败;而物理永不过期只关心 key 存在与否,格式变更影响可控
- 最易被忽略的点:Redis
KEYS命令在大数据量下会阻塞,但后台刷新线程常依赖它扫描 hot_* key —— 必须改用SCAN+ 游标分片,且每次 SCAN 控制在 100 以内,否则触发 slowlog


















