缓存三大问题(击穿、穿透、雪崩)原理及解决方案是什么?
摘要:缓存的击穿、穿透、雪崩是后端开发中高频出现、极易引发服务雪崩的核心问题,三者看似相似但原理和解决方案完全不同。本文用「原理+场景+落地方案+代码」的形式,一次性讲透,看完就能直接落地到生
缓存的击穿、穿透、雪崩是后端开发中高频出现、极易引发服务雪崩的核心问题,三者看似相似但原理和解决方案完全不同。本文用「原理+场景+落地方案+代码」的形式,一次性讲透,看完就能直接落地到生产环境。
先分清:三大问题核心区别(一张表看懂)
问题类型
核心定义
典型场景
直接后果
缓存穿透
请求根本不存在的key,缓存和DB都查不到
恶意攻击(批量查不存在的用户ID)、业务bug(传错参数)
DB被大量无效请求打满,连接耗尽
缓存击穿
热点key突然失效(过期/被删除),大量请求瞬间打到DB
秒杀商品key过期、热门榜单key被删除
DB单点压力骤增,瞬间超时/宕机
缓存雪崩
大量缓存key同时失效,或缓存服务整体宕机
缓存key集中设置相同过期时间、Redis集群宕机
DB被海量请求压垮,整个服务不可用
一、缓存穿透:查「不存在的key」把DB打崩
1. 原理
请求的key在缓存中不存在,且在数据库中也不存在,导致每次请求都「穿透」缓存直接打在DB上。如果是恶意高频请求(比如每秒上万次查不存在的用户ID),DB很快会被压垮。
2. 全套解决方案(按优先级排序)
方案1:参数校验(最基础,成本最低)
在请求到达缓存/DB前,先做合法性校验,过滤掉明显无效的请求:
比如用户ID是正整数,直接拦截负数/0/超长字符串;
比如商品ID有固定格式,拦截不符合格式的请求。
代码示例(Java):
public User getUserById(Long userId) {
// 第一步:参数合法性校验,直接拦截无效请求
if (userId == null || userId <= 0 || userId > 10000000) {
return null;
}
// 后续缓存/DB查询逻辑...
}
方案2:空值缓存(核心方案)
对查询结果为空的key,也写入缓存(设置极短的过期时间,比如1-5分钟),避免后续请求重复打DB。
代码示例(Java + Redis):
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查缓存
String userJson = redisTemplate.opsForValue().get(cacheKey);
// 空值标记(避免JSON解析问题,用特定字符串表示空)
if ("NULL".equals(userJson)) {
return null;
}
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 2. 缓存未命中,查DB
User user = userMapper.selectById(userId);
if (user == null) {
// 3. 空值写入缓存,设置短过期时间(防止恶意攻击占满缓存)
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
return null;
}
// 4. 正常数据写入缓存
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
return user;
}
方案3:布隆过滤器(高并发/海量key场景)
如果无效key的范围极大(比如电商场景查不存在的商品ID),用布隆过滤器提前判断key是否存在,不存在则直接返回,完全拦截无效请求。
核心逻辑:
启动时将DB中所有有效key加载到布隆过滤器;
请求过来先过布隆过滤器,不存在则直接返回;
存在则走正常缓存/DB流程。
