随机TTL和逻辑过期可叠加使用以缓解缓存击穿:在基础TTL上加不超过20%的随机偏移避免过期时间对齐;同时在缓存值中嵌入expireAt字段实现业务层过期控制,并异步刷新,二者结合可显著降低击穿风险。

用随机TTL分散热点Key的失效时间点
缓存击穿本质是大量请求在同一毫秒级时间点发现同一个Key过期,然后集体冲向数据库。固定TTL(比如统一设为10分钟)会让预热、刷新、自然过期全部对齐,风险极高。
实际操作中,应在基础过期时间上叠加一个随机偏移量。例如基础TTL是600秒,就加 Math.random() * 120 秒,最终写入Redis时用 SET key value EX 687 这类带随机秒数的命令。
- 偏移量不宜过大(一般不超过基础TTL的20%),否则会显著拉长部分数据的陈旧周期
- 不要在应用层用
Thread.sleep()模拟延迟——这解决不了并发穿透,只是把问题藏得更深 - Spring Boot中若用
StringRedisTemplate,需手动拼接EX参数,opsForValue().set(key, value, Duration.ofSeconds(base + offset))是安全写法
避免业务语义裸露导致Key被暴力枚举
像 user:123、shop:456 这类直接暴露ID的Key设计,容易被爬虫或攻击者按序遍历,一旦缓存未命中,就会形成定向穿透。这不是击穿的主因,但会放大击穿影响面。
更稳妥的做法是混淆ID或引入业务上下文前缀。例如把 user:123 改成 user:prod:9a3f(其中 9a3f 是123经简单哈希或Base32编码的结果),或者加上租户标识 user:tenant_a:123。
- 不推荐MD5/SHA类强哈希——它们不可逆且长度固定,不利于调试和日志排查
- 避免使用时间戳作为Key后缀(如
user:123:1720847340),这反而制造了新的“瞬时热点” - 如果必须保留可读性,至少在接入层做一次ID白名单校验,非法ID直接拦截,不走缓存逻辑
为高频读场景预留“逻辑过期”字段结构
物理过期(TTL)一到,Key就被Redis自动删除,这是击穿的触发开关。而逻辑过期把“是否过期”的判断权收回到业务代码里,让缓存值本身携带一个时间戳字段,比如存成JSON:{"data": {...}, "expireAt": 1720847340}。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
查询时先取整个结构,解析出 expireAt,再比对当前时间。若已逻辑过期,就异步触发更新,但依然返回旧数据;只有首次访问且逻辑过期时,才需要加锁重建。
- 不能只存
expireAt而不存真实数据——那样就退化成空缓存,失去意义 - 异步更新失败必须有重试机制,否则逻辑过期会变成永久过期
- 这种结构不适合用
StringRedisTemplate直接操作,建议封装成专用工具类,避免每次都要手写JSON序列化
慎用“永不过期”,除非你能控制更新节奏
把热点Key设为永不过期(即不设EX/PX)看似一劳永逸,但隐患在于:一旦数据在数据库中变更,缓存就彻底失联,除非你有可靠、低延迟的主动刷新通道。
常见翻车场景包括:定时任务挂掉、消息队列堆积、监听binlog的组件宕机。此时用户看到的是几分钟甚至几小时前的脏数据,比击穿更难排查。
- 永不过期只适合静态配置类数据(如省市区字典、APP版本号),且必须配套监控其更新成功率
- 电商商品详情这类动态数据,宁可接受小概率击穿,也不要盲目设永不过期
- 如果真要用,务必搭配
DEL+SET原子更新,而不是SET覆盖——后者在并发下可能写入中间态
逻辑过期和随机TTL不是二选一,而是可以叠加使用的组合。真正危险的,是既没随机TTL、又没逻辑过期、还裸露ID的三无Key设计。

















