上篇 Redis 入门里写了基本操作,这次聊聊缓存里最头疼的三个问题。先说个真实场景——公司有个活动页面,查的是热门商品的库存,流量一大数据库就扛不住。加了缓存之后好些了,结果有一天活动刚上线,缓存全挂了。
缓存击穿
现象:一个热点 key 过期了,瞬间大量请求打到数据库。
实际的画面是这样的:缓存 key 到期→第一个请求过来缓存 miss→去查数据库→还没查完第二个请求又来了,缓存还是 miss→十几个请求全打在数据库的同一行数据上。
解法:最简单的就是用互斥锁,拿到锁的去查数据库,其他的等着:
String get(String key) {
String val = redis.get(key);
if (val != null) return val;
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1")) {
redis.expire(lockKey, 30);
val = db.query(key);
redis.set(key, val, 300);
redis.del(lockKey);
return val;
}
Thread.sleep(100);
return get(key); // 重试
}
核心思路:让缓存 miss 之后只有一个线程去加载数据。缺点是代码复杂度上来了,而且第一个请求要等数据库查询。
缓存穿透
现象:客户端查一个根本不存在的数据,缓存和数据库都没有,每次请求都穿过去打数据库。
比如有人拿 userId=-1 一直刷接口,缓存里没有 -1,数据库也没有 -1,每次都会走到数据库查询那一步。
解法:布隆过滤器或者缓存空值。
请求 userId=-1
│
▼
┌──────────────────┐
│ 布隆过滤器 │
│ 是否存在此 ID? │
└────┬────────┬────┘
│YES │NO
▼ ▼
查缓存 直接返回空
布隆过滤器可以告诉你"这个 key 一定不存在",但会误判"可能存在"。适合用来挡掉大量不存在的 key。
更简单的方案是缓存空值,redis.set("user:-1", "null", 60),60 秒过期。缺点是被恶意攻击时空值也会占内存。
缓存雪崩
现象:大量缓存 key 在同一时间过期,或者 Redis 宕机。
和击穿的区别:击穿是一个热点 key 过期,雪崩是一大片 key 同时过期。
解法有两个方向:
- 过期时间加随机值:原本统一 5 分钟过期,改成 5 分钟 ± 随机 60 秒,避免集中在同一时刻失效。
int ttl = 300 + ThreadLocalRandom.current().nextInt(60);
redis.set(key, value, ttl);
- Redis 集群 + 熔断降级:Redis 不要单机部署,用哨兵或集群模式。如果 Redis 真的全挂了,就打开限流开关直接返回兜底数据,不要把流量全部压到数据库。
写在最后
三个"缓"问题归根到底是一个思路:**永远不要信任缓存。**缓存是数据库的护城河,但护城河自己也可能出事。每次做缓存方案的时候都要问自己:如果缓存挂了或没有命中,数据库抗不扛得住?
代码上的各种锁、布隆、随机 TTL,本质上都是为了让缓存"可预期的失败",而不是放任流量乱打。
评论