文章目录
- [Redis 缓存三大经典问题:穿透、击穿、雪崩 原理剖析与生产级落地指南](#Redis 缓存三大经典问题:穿透、击穿、雪崩 原理剖析与生产级落地指南)
-
- 导读
- 一、问题根源:所有缓存问题的本质
- 二、概念辨析:穿透、击穿、雪崩的核心区别
- 三、分场景解决方案深度拆解
-
- [3.1 缓存穿透:拦截无效请求,从源头减少DB压力](#3.1 缓存穿透:拦截无效请求,从源头减少DB压力)
-
- 方案1:空值缓存
- [方案2:布隆过滤器(Bloom Filter)](#方案2:布隆过滤器(Bloom Filter))
- 方案3:接口层参数校验
- [3.2 缓存击穿:热点key过期,如何避免瞬时并发打库](#3.2 缓存击穿:热点key过期,如何避免瞬时并发打库)
-
- 方案1:分布式互斥锁
- 方案2:逻辑过期时间
- [方案3:热点数据永久有效 + 异步更新](#方案3:热点数据永久有效 + 异步更新)
- [3.3 缓存雪崩:大面积失效,如何保障数据库不垮](#3.3 缓存雪崩:大面积失效,如何保障数据库不垮)
- [四、代码落地:核心方案 Java 实现](#四、代码落地:核心方案 Java 实现)
-
- [4.1 布隆过滤器(Guava 实现)](#4.1 布隆过滤器(Guava 实现))
- [4.2 分布式互斥锁解决缓存击穿](#4.2 分布式互斥锁解决缓存击穿)
- 五、生产环境避坑与最佳实践
- 六、总结与选型建议
- 七、附加一个redis通用类
Redis 缓存三大经典问题:穿透、击穿、雪崩 原理剖析与生产级落地指南
导读
在后端开发中,Redis 缓存是提升系统性能、承接高并发流量的核心组件。但缓存架构从落地第一天起,就必然面临三类经典问题:缓存穿透、缓存击穿、缓存雪崩。三者概念相似但触发逻辑、影响范围、解决方案完全不同,既是面试高频考点,也是生产环境必须解决的架构风险点。
本文系统梳理三类问题的核心区别,逐一拆解主流解决方案的原理、优缺点与适用场景,并附 Java 实现代码与生产环境避坑经验,既可作为面试备考资料,也可直接用于项目架构优化。
一、问题根源:所有缓存问题的本质
缓存的标准请求链路非常清晰:
- 业务请求到达系统,优先查询 Redis 缓存;
- 若缓存命中,直接返回数据,请求结束;
- 若缓存未命中,再查询数据库,查询成功后将数据回写缓存,最终返回结果。
这套机制的核心价值,是让绝大多数请求被缓存承接,只有少量请求透传到数据库。而所有缓存问题的本质,都是大量请求绕过缓存直接冲击数据库,导致数据库连接耗尽、压力骤增,严重时会引发服务雪崩。
穿透、击穿、雪崩的差异,只在于「请求绕过缓存的原因」和「影响范围」不同。
二、概念辨析:穿透、击穿、雪崩的核心区别
很多人容易混淆这三个概念,我们可以通过一张表快速厘清边界:
| 问题类型 | 触发场景 | 核心特征 | 影响范围 | 典型业务场景 |
|---|---|---|---|---|
| 缓存穿透 | 查询数据库中根本不存在的数据 | 缓存永远不命中,每次请求都直达数据库 | 全量无效请求持续打库 | 恶意攻击用无效id遍历接口、前端传参异常 |
| 缓存击穿 | 某一个热点key突然过期失效 | 单点高热key失效瞬间,并发请求同时打库 | 单条数据对应的流量集中冲击 | 爆款商品详情页、活动首页、热搜话题 |
| 缓存雪崩 | 大量key同时过期,或Redis整体宕机 | 大面积缓存集体失效,数据库遭遇流量洪峰 | 全量/批量数据流量冲击 | 批量设置相同过期时间、Redis节点故障宕机 |
简单总结:
- 穿透:查的是「不存在的数据」,缓存从一开始就挡不住;
- 击穿:查的是「存在的热点数据」,只是刚好过期了;
- 雪崩:不是一个key的问题,是「一大批key都失效了」。
三、分场景解决方案深度拆解
3.1 缓存穿透:拦截无效请求,从源头减少DB压力
缓存穿透的核心矛盾是:数据不存在,所以缓存永远无法命中,请求会持续打到数据库。针对这个问题,主流有三层防御方案。
方案1:空值缓存
对数据库查询结果为空的key,在Redis中缓存一个空标识(如null、空字符串、自定义占位符),并设置较短的过期时间(通常30-60秒)。后续短时间内对该key的请求,会直接从缓存拿到空结果,不会再访问数据库。
- 优点:实现成本极低,改动最小,立竿见影;
- 缺点:会占用额外Redis内存,且缓存期间若数据库新增了对应数据,会存在短期数据不一致。
- 适用场景:低频无效请求、普通业务系统的基础防护。
方案2:布隆过滤器(Bloom Filter)
将所有存在的数据主键预先存入布隆过滤器,请求到达时先经过过滤器校验:如果过滤器判定key不存在,直接拦截返回,根本不会走到缓存和数据库层。
布隆过滤器的核心原理是通过多个哈希函数将数据映射到一个位图上,它的判定规则是:判定不存在,则一定不存在;判定存在,则可能存在,存在固定的误判率。
- 优点:内存占用极小(百万级数据仅需几MB内存),拦截效率高,适合大数据量场景;
- 缺点:存在固定误判率,原生不支持数据删除,数据新增时需要同步更新过滤器。
- 适用场景:数据量庞大、存在恶意攻击风险的系统,如电商商品库、用户信息查询。
方案3:接口层参数校验
在系统入口层对请求参数做合法性校验,比如id为负数、格式不符合规则、长度异常的请求,直接在Controller层返回错误,这是成本最低的第一道防线。
3.2 缓存击穿:热点key过期,如何避免瞬时并发打库
缓存击穿的核心矛盾是:热点key过期的瞬间,大量并发请求同时发现缓存失效,一起去查数据库。解决方案的核心思路,要么是「只让一个请求去重建缓存」,要么是「不让缓存突然失效」。
方案1:分布式互斥锁
缓存失效时,只允许一个请求获取分布式锁并查询数据库、重建缓存,其余请求阻塞等待一段时间后重试。
- 优点:数据一致性强,缓存重建完成后所有请求都能拿到最新数据;
- 缺点:存在性能损耗,大量请求阻塞会降低吞吐量,锁设置不当有死锁风险。
- 适用场景:数据一致性要求较高、并发量中等的热点数据场景。
方案2:逻辑过期时间
不在Redis中设置key的物理过期时间,而是在缓存的value里额外存储一个逻辑过期时间。请求读取缓存时,判断逻辑时间是否过期:
- 未过期:直接返回缓存数据;
- 已过期:当前线程直接返回旧值,同时异步启动一个后台线程去重建缓存。
- 优点:无线程阻塞,吞吐量高,不会出现瞬时流量打库;
- 缺点:存在短期数据不一致,内存占用略高,需要维护异步线程。
- 适用场景:对可用性要求极高、允许短暂数据不一致的热点场景,如首页推荐、商品展示。
方案3:热点数据永久有效 + 异步更新
对顶级热点数据(如活动主会场、平台首页),直接设置缓存永久不过期,通过消息队列、定时任务或数据变更事件触发异步更新缓存。
- 优点:从根源上避免了过期导致的击穿问题;
- 缺点:需要额外维护更新机制,极端情况下会出现长时间数据不一致。
3.3 缓存雪崩:大面积失效,如何保障数据库不垮
缓存雪崩分为两种场景:一是大量key同时过期,二是Redis服务整体宕机。解决方案需要从「避免同时失效」和「故障兜底」两个维度入手。
方案1:过期时间随机打散
在设置缓存过期时间时,在基础时间上增加一个随机偏移量(如基础30分钟 + 0~5分钟随机值),让key的过期时间均匀分布,避免大量key在同一时刻集体失效。
这是成本最低、性价比最高的方案,也是所有缓存系统的基础配置。
方案2:多级缓存架构
构建「应用层本地缓存 + Redis分布式缓存」的两级架构:
- 请求先查应用本地缓存(如Caffeine、Guava Cache),命中直接返回;
- 本地缓存未命中,再查Redis;
- Redis也未命中,才查询数据库。
当Redis出现故障或大面积失效时,本地缓存能承接一部分流量,起到兜底缓冲的作用。
方案3:服务熔断与降级
在数据库访问层增加熔断机制,当数据库访问量、失败率达到阈值时,触发熔断,直接返回预设的兜底数据或降级页面,主动拒绝部分流量,保护数据库不被打垮。
方案4:Redis高可用集群
通过主从+哨兵、Redis Cluster集群模式部署,避免Redis单点故障。单节点宕机时,从节点可以快速切换为主节点,从架构层面降低Redis整体故障的概率。
四、代码落地:核心方案 Java 实现
以下基于 Spring Boot + RedisTemplate 环境,提供两个核心方案的可复用代码。
4.1 布隆过滤器(Guava 实现)
适合中小规模数据量的快速落地,无需引入额外中间件。
java
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ProductService {
@Resource
private ProductMapper productMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 预计存放100万条数据,误判率设置为0.01
private BloomFilter<Long> bloomFilter;
// 项目启动时初始化布隆过滤器
@PostConstruct
public void initBloomFilter() {
bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
// 加载全量有效数据id
List<Long> allValidIds = productMapper.listAllValidIds();
allValidIds.forEach(bloomFilter::put);
}
public Product getProductById(Long productId) {
// 第一层:布隆过滤器拦截不存在的请求
if (!bloomFilter.mightContain(productId)) {
return null;
}
// 第二层:查询Redis缓存
String cacheKey = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 第三层:查询数据库
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
} else {
// 空值缓存,防止穿透
redisTemplate.opsForValue().set(cacheKey, null, 60, TimeUnit.SECONDS);
}
return product;
}
}
4.2 分布式互斥锁解决缓存击穿
java
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class HotProductService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ProductMapper productMapper;
public Product getHotProduct(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 先查缓存
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
try {
// 2. 缓存未命中,尝试获取分布式锁
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lockAcquired)) {
// 3. 获取锁成功,双重检查缓存(避免重复构建)
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product == null) {
// 4. 查询数据库并回写缓存
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
}
}
} else {
// 5. 未获取到锁,休眠后重试
Thread.sleep(100);
return getHotProduct(productId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 6. 释放锁(生产环境建议用Lua脚本保证原子性)
redisTemplate.delete(lockKey);
}
return product;
}
}
五、生产环境避坑与最佳实践
-
空值缓存必须设置过期时间
不设置过期时间会导致无效key永久堆积,占用Redis内存,甚至引发内存溢出。建议空值过期时间控制在1分钟以内。
-
分布式锁释放必须保证原子性
上述代码中的直接删除锁存在风险:如果锁超时自动释放后,其他线程获取了锁,当前线程再删除就会误释放别人的锁。生产环境推荐使用 Lua 脚本校验锁的持有者后再删除。
-
大促/活动前必须做热点key预热
活动开始前,提前将热点数据批量载入缓存,避免活动启动瞬间大量请求同时发现缓存未命中,集中冲击数据库。
-
不要依赖单一方案,构建多层防御体系
一套完善的缓存防护体系,应该是分层递进的:
接口参数校验 → 布隆过滤器 → 缓存层(过期打散/逻辑过期) → 分布式锁 → 服务熔断降级 → 数据库高可用
六、总结与选型建议
缓存问题没有银弹,不同业务规模、不同一致性要求,对应的最优方案完全不同:
- 小型业务系统:参数校验 + 空值缓存 + 过期时间随机打散,即可覆盖绝大多数场景;
- 中型业务系统:在此基础上增加布隆过滤器、分布式互斥锁,配合服务熔断降级;
- 大型高并发系统:需要构建多级缓存架构 + Redis高可用集群 + 全链路流量防护。
理解每个方案的底层原理,结合业务场景做组合选型,才是缓存架构设计的核心。
七、附加一个redis通用类
java
/**
* @author yxz
* @description RedisUtils Redis工具类
* * 封装常用的Redis操作
* @date 2026-06-19 0:12
*/
@Component
@RequiredArgsConstructor
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
private final RedisProperties redisProperties;
// ==================== 通用操作 ====================
/**
* 指定缓存过期时间
*
* @param key 键
* @param time 时间(秒)
* @return 是否成功
*/
public boolean expire(String key, long time) {
return expire(key, Duration.ofSeconds(time));
}
/**
* 指定缓存过期时间
*
* @param key 键
* @param duration 时间
* @return 是否成功
*/
public boolean expire(String key, Duration duration) {
try {
if (duration.getSeconds() > 0) {
redisTemplate.expire(key, duration);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key获取过期时间
*
* @param key 键
* @return 过期时间(秒),返回-1表示永久有效,-2表示键不存在
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return 是否存在
*/
public boolean hasKey(String key) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 键,可以传一个或多个
*/
public void delete(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
// ==================== String操作 ====================
/**
* 获取缓存
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 获取缓存(泛型)
*
* @param key 键
* @param clazz 类型
* @return 值
*/
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> clazz) {
Object value = get(key);
if (value == null) {
return null;
}
if (clazz.isInstance(value)) {
return (T) value;
}
return null;
}
/**
* 设置缓存
*
* @param key 键
* @param value 值
* @return 是否成功
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 设置缓存并指定过期时间
*
* @param key 键
* @param value 值
* @param time 时间(秒),如果time小于等于0将设置无限期
* @return 是否成功
*/
public boolean set(String key, Object value, long time) {
return set(key, value, Duration.ofSeconds(time));
}
/**
* 设置缓存并指定过期时间
*
* @param key 键
* @param value 值
* @param duration 时间
* @return 是否成功
*/
public boolean set(String key, Object value, Duration duration) {
try {
if (duration.getSeconds() > 0) {
redisTemplate.opsForValue().set(key, value, duration);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 递增因子
* @return 递增后的值
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 递减因子
* @return 递减后的值
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ==================== Hash操作 ====================
/**
* HashGet
*
* @param key 键
* @param item 项
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return 是否成功
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet并设置过期时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return 是否成功
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return 是否成功
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒),如果已存在的hash表有时间,这里会替换原有的时间
* @return 是否成功
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键
* @param item 项,可以传多个
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键
* @param item 项
* @return 是否存在
*/
public boolean hHasKey(String key, String item) {
return Boolean.TRUE.equals(redisTemplate.opsForHash().hasKey(key, item));
}
/**
* hash递增
*
* @param key 键
* @param item 项
* @param by 递增因子
* @return 递增后的值
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 递减因子
* @return 递减后的值
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ==================== List操作 ====================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束,-1代表所有值
* @return 列表内容
*/
public List<Object> lrange(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return 长度
*/
public long llen(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引获取list中的值
*
* @param key 键
* @param index 索引
* @return 值
*/
public Object lindex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将值放入list头部
*
* @param key 键
* @param value 值
* @return 是否成功
*/
public boolean lpush(String key, Object value) {
try {
redisTemplate.opsForList().leftPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将值放入list尾部
*
* @param key 键
* @param value 值
* @return 是否成功
*/
public boolean rpush(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除list中第一个值为value的元素
*
* @param key 键
* @param count 移除数量
* @param value 值
* @return 移除的数量
*/
public long lrem(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ==================== Set操作 ====================
/**
* 获取Set中的所有元素
*
* @param key 键
* @return 元素集合
*/
public Set<Object> smembers(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 判断value是否在Set中
*
* @param key 键
* @param value 值
* @return 是否存在
*/
public boolean sismember(String key, Object value) {
try {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入Set
*
* @param key 键
* @param values 值,可以传多个
* @return 成功放入的个数
*/
public long sadd(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除Set中的元素
*
* @param key 键
* @param values 值,可以传多个
* @return 移除的个数
*/
public long srem(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取Set的大小
*
* @param key 键
* @return 大小
*/
public long scard(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}