缓存穿透时应写入空值占位:DB查无结果则setex key 60 "NULL",应用层识别后转null;本地缓存同理用Optional.empty(),避免重复查询。

缓存穿透时 get 返回 null 怎么办
Redis 本身不区分“缓存未命中”和“业务数据确实为空”,get 拿不到就直接穿透到 DB。这在多级缓存里会放大问题——比如本地缓存(Caffeine)也查不到,请求全涌向 Redis,再涌向 DB。
解决思路不是加锁或布隆过滤器优先,而是统一用空值占位:
- DB 查无结果时,写入
setex key 60 "NULL"(TTL 缩短,避免长期污染) - 应用层读到
"NULL"字符串,主动转成null或业务空对象,不继续往下查 - 本地缓存同理:
cache.put(key, Optional.empty()),别让它反复问 Redis
注意:别用 "" 或 0 占位,容易和真实业务值混淆;也别设过长 TTL,60 秒足够覆盖大部分热点空查场景。
本地缓存和 Redis 的 TTL 怎么配才不互相打架
两级缓存的 TTL 如果随便设,会出现“本地还没过期,Redis 已更新,结果读到脏数据”或者“本地刚过期,Redis 也过期,瞬间打穿 DB”。
关键不是同步,而是错峰 + 主动刷新:
- 本地缓存 TTL 设为 Redis 的
70%(比如 Redis 是 300s,本地设 210s),让它先过期,触发一次 Redis 查询 - 查询 Redis 后,立刻用新值+新 TTL 刷新本地缓存(不是等下次访问才加载)
- Redis 的 TTL 要带随机偏移,比如
300 ± 30,防止大量 key 同时失效引发雪崩
别依赖 expire 精确控制——Redis 过期是惰性+定期混合策略,实际失效时间有浮动。
并发重建缓存时 setnx + expire 为什么还是失败
setnx 成功但 expire 失败,会导致 key 永不过期,后续所有请求都卡在空值等待,这是经典竞态漏洞。
必须原子操作:
- Redis 2.6.12+ 用
set key value ex 300 nx,一条命令搞定 - 老版本只能用 Lua 脚本:
redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2], "NX") - 本地缓存层也要做类似保护:Caffeine 的
refreshAfterWrite比expireAfterWrite更适合抗抖动,它不阻塞读,后台异步刷新
别图省事拆成两步命令,网络分区或进程中断时,setnx 成功但 expire 没发出去,这个 key 就永远卡死。
流量突增时,本地缓存击穿比 Redis 击穿更危险
很多人盯着 Redis 是否扛得住,却忽略本地缓存(如 Caffeine)在单机 QPS 上千时,如果没限流,一个 key 击穿可能瞬间拉起上百个线程查 DB——而 Redis 集群还能分摊,单机本地缓存没有兜底。
必须在本地缓存层加轻量熔断:
- 用
Caffeine.newBuilder().maximumSize(10000).recordStats()开启统计,监控evictionCount和missRate - 对高危 key(比如用户中心、商品详情),封装一层
loadingCache.get(key, k -> loadWithSemaphore(k)),用信号量限制并发加载数 - 别让本地缓存“自动加载”失控——
CacheLoader必须包裹异常兜底,否则一次 DB 超时可能导致整个缓存 loader 停摆
最易被忽略的是:本地缓存的 GC 压力。大对象频繁 put/remove 会触发 Young GC,反而拖慢响应——key 对应的 value 要尽量小,别把整张订单 JSON 塞进去。










