这三个词经常被混着说,但成因完全不同,处理方式也不一样。分清楚之后,线上出事才知道该动哪里。
穿透:查一个根本不存在的数据
请求带着不存在的 id 打进来,缓存没有,落到数据库也没有,于是每次请求都穿透到 DB。如果有人拿随机 id 刷接口,数据库很快就顶不住。
两种处理:
- 缓存空值。查不到也写一个短 TTL 的空标记(比如 60 秒),下次同样的 id 直接在缓存层返回。实现最简单,代价是要防住大量不同 id 的攻击——那就得配合限流。
- 布隆过滤器。把所有存在的 id 预热进去,请求先过过滤器,明确不存在的直接拒掉。注意布隆过滤器有假阳性没有假阴性,说“不存在”一定不存在,说“存在”可能误判。
击穿:热点 key 过期的瞬间
某个商品详情页 QPS 很高,缓存 TTL 到期的同一秒,几千个请求同时发现缓存没了,一起去查库。数据库瞬间被打出连接池上限。
解法是加锁,只放一个请求去回源:
def get(key):
v = redis.get(key)
if v is not None:
return v
lock = redis.set(f"lock:{key}", "1", nx=True, ex=5)
if lock:
try:
v = db_query(key)
redis.set(key, v, ex=300)
finally:
redis.delete(f"lock:{key}")
return v
time.sleep(0.05)
return get(key) # 等待后重试,实际要加最大次数限制
更省事的做法是逻辑过期:value 里存一个业务过期时间,物理上不过期,发现逻辑过期就异步刷新,先返回旧值。牺牲一点一致性换稳定性。
雪崩:大量 key 同时失效
要么是统一预加载、TTL 设成同一个值,要么是 Redis 整个挂了。前者在 TTL 上加随机抖动就能解决:
expire = 300 + random.randint(0, 60)
后者要靠架构:Redis 主从 + 哨兵、本地二级缓存兜底、DB 前的限流。真挂了的时候,能返回降级页面比全部超时要好。
顺序别搞反
先分清是哪种问题,再动手。给所有 key 加随机 TTL 解决不了穿透,加布隆过滤器也挡不住雪崩。我见过一个项目三种方案全上了一层,结果缓存逻辑复杂到没人敢改——问题其实只有一个:热点 key 的过期时间太集中。
- THE END -
最后修改:2022年3月6日
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/654
共有 0 条评论