作者 lucy · 2026-04-13
⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。
缓存是后端系统的标配,但缓存用不好会引发三种经典问题:穿透、击穿、雪崩。这三个名字听起来吓人,但其实每一种都有成熟的解决方案。本文记录我踩过的坑和对应的处理思路。
场景一:缓存穿透 — 数据库里查不到,还每次都来问
大量请求访问一个根本不存在的数据(比如恶意刷接口),缓存里没有,数据库里也没有,每次都打到 DB,轻则接口变慢,重则 DB 被打死。
判断方法:日志里大量相同 key 查不到,DB 负载异常升高。
解决方案(仅供参考)
方案A:布隆过滤器(BloomFilter)
在访问缓存之前,先用布隆过滤器判断这个 key 存不存在。如果不存在,直接返回,不再查 DB。
# 布隆过滤器判断
def get_user(id):
if not bloom_filter.contains(id):
return None # 一定不存在,直接返回
# 存在的情况下才查缓存,再查 DB
cache = redis.get(f"user:{id}")
if cache:
return json.loads(cache)
user = db.query("SELECT * FROM users WHERE id = %s", id)
if user:
redis.setex(f"user:{id}", 3600, json.dumps(user))
return user
方案B:空值缓存
对于已确认不存在的 key,也写入缓存,但设置较短的过期时间。
user = db.query("SELECT * FROM users WHERE id = %s", id)
if not user:
# 空值缓存5分钟,防止短期内重复查询
redis.setex(f"user:{id}", 300, "")
else:
redis.setex(f"user:{id}", 3600, json.dumps(user))
return user
场景二:缓存击穿 — 热点 key 过期,大量请求同时打 DB
某个热点 key 突然过期,而此时有大量并发请求同时涌入,这些请求全部打到数据库,DB 瞬间被压垮。
这和穿透的区别是:数据是存在的,只是 key 过期了。
解决方案(仅供参考)
方案:互斥锁(Mutex)+ 单一提前重建
import threading, time
def get_user(id):
cache_key = f"user:{id}"
cache = redis.get(cache_key)
if cache:
return json.loads(cache)
# 加互斥锁,只允许一个请求查DB
lock_key = f"lock:{id}"
if redis.set(lock_key, "1", nx=True, ex=10):
try:
user = db.query("SELECT * FROM users WHERE id = %s", id)
redis.setex(cache_key, 3600, json.dumps(user))
return user
finally:
redis.delete(lock_key)
else:
# 等待其他请求写入缓存后重试
time.sleep(0.1)
return get_user(id)
return None
也可以用 Redis 的 SETNX + EXPIRE 组合实现分布式锁。
场景三:缓存雪崩 — 大量 key 同时过期,请求全部打到 DB
大量缓存 key 同一时间过期(比如系统重启后、故障恢复后),所有请求同时发现缓存失效,全部涌入 DB。
解决方案(仅供参考)
方案A:过期时间加随机偏移
# 不要所有 key 都设整点过期
expire_time = 3600 + random.randint(0, 300) # 1小时~1小时05分随机
redis.setex(cache_key, expire_time, value)
方案B:多级缓存(Local + Redis)
本地缓存兜底,Redis 失效时先返回本地缓存数据,避免全部打到 DB。
import cachetools
# 本地LRU缓存,容量100,过期5分钟
local_cache = cachetools.LRUCache(maxsize=100, ttl=300)
def get_user(id):
if id in local_cache:
return local_cache[id]
cache_key = f"user:{id}"
cache = redis.get(cache_key)
if cache:
user = json.loads(cache)
local_cache[id] = user
return user
user = db.query("SELECT * FROM users WHERE id = %s", id)
if user:
redis.setex(cache_key, 3600, json.dumps(user))
local_cache[id] = user
return user
方案C:Redis 持久化 + 预热
服务重启前,主动从 DB 加载热点数据到缓存,避免冷启动大量击穿。
三种问题对比
| 问题 | 原因 | 核心解法 |
|---|---|---|
| 缓存穿透 | 数据根本不存在 | 布隆过滤器 / 空值缓存 |
| 缓存击穿 | 热点 key 过期 | 互斥锁 + 单一重建 |
| 缓存雪崩 | 大量 key 同时过期 | 过期时间加随机偏移 + 多级缓存 |
经验总结
-
穿透的根因是”不存在的数据”,优先布隆过滤器。 它空间效率极高,判断不存在的 key 非常快,内存占用是哈希表的 1/10。
-
击穿的核心是”并发争抢同一个 key”,互斥锁是标准解法。 注意锁的粒度要细,否则会退化成全局串行。
-
雪崩的本质是”时间集中”,加随机偏移是最简单的方案。 改动最小,效果最直接。
-
生产环境建议:布隆过滤器 + 多级缓存 + 过期随机偏移 三件套一起上,三种问题同时兜底。
-
监控是最后一道防线。 缓存命中率、DB 负载、接口 P99 延迟,这些指标要盯紧,出了问题才能第一时间发现。
⚠️ 再声明:以上方案均有各自适用条件,分布式锁实现复杂度较高,布隆过滤器需要维护额外数据结构,建议根据实际业务量评估后使用。
有问题欢迎留言交流 🚀