在高并发场景下,Redis 作为缓存层,主要目的是减轻数据库压力,提升系统响应速度。然而,如果使用不当,可能会出现三种典型的缓存问题:缓存穿透、缓存击穿 和 缓存雪崩。下面分别解释它们的定义及应对策略。
一、缓存穿透
定义
缓存穿透指查询一个根本不存在的数据,缓存层和存储层都没有命中。由于缓存不命中,每次请求都会穿透到数据库,导致数据库压力骤增,甚至被恶意攻击利用。
典型场景
- 用户查询一个不存在的商品ID。
- 攻击者构造大量不存在的key发起请求。
应对策略
- 布隆过滤器
在缓存前加一层布隆过滤器,将所有可能存在的数据哈希映射到位数组中。查询时先经过布隆过滤器,如果判定为不存在,则直接返回,不再访问数据库。
优点:内存占用小,查询快。
缺点:存在一定误判率(可能将不存在判定为存在),需定期重建过滤器。 - 缓存空对象
当数据库查询结果为空时,也将这个空结果(如 null)缓存起来,并设置一个较短的过期时间(如几分钟)。这样后续相同请求直接从缓存返回,避免穿透到数据库。
优点:实现简单,可解决大部分穿透问题。
缺点:会占用少量缓存空间,若攻击者构造大量不同key,仍会缓存大量空对象。 - 接口层参数校验
在入口层对请求参数进行合法性校验(如ID不能为负数、长度限制等),提前拦截非法请求,减少无效穿透。
二、缓存击穿
定义
缓存击穿指某个热点数据在缓存中过期失效的瞬间,恰好有大量并发请求同时访问该数据。由于缓存未命中,所有请求都打到数据库,导致数据库瞬时压力过大。
典型场景
- 某个热门商品详情页的缓存过期,大量用户同时刷新导致数据库被击穿。
应对策略
-
互斥锁(Mutex Key)
当缓存失效时,不是所有线程都去加载数据,而是只允许一个线程去数据库查询并重建缓存,其他线程等待。可以使用 Redis 的SETNX或 Redisson 等分布式锁实现。
示例流程:- 请求A发现缓存未命中,尝试获取锁。
- 若获取成功,则去数据库加载数据并写入缓存,最后释放锁。
- 若获取锁失败,则短暂休眠后重试,直到从缓存中获取数据。
-
逻辑过期(提前异步刷新)
不设置物理过期时间,而是给数据添加一个逻辑过期时间字段。当查询时发现逻辑时间已过,则返回旧数据,同时异步去数据库加载新数据并更新缓存。这样可以保证请求始终能命中缓存,不会击穿数据库。 -
热点数据永不过期
对于热点数据,可以设置物理上永不过期,但在后台定时或由更新事件触发刷新。结合互斥锁或异步更新,确保数据一致性。
三、缓存雪崩
定义
缓存雪崩指缓存层出现大规模失效或整体不可用,导致大量请求直接涌向数据库,造成数据库压力骤增甚至宕机。
常见原因
- 大量缓存 key 在同一时间点过期(如设置了相同的过期时间)。
- Redis 实例宕机,或集群不可用。
应对策略
1. 针对大量 key 同时过期
- 过期时间随机化
设置缓存过期时间时,在一个基础值上加上随机偏移量(如 5~10 分钟),避免大量 key 同时失效。 - 热点数据分散过期
对热点数据单独设计失效策略,比如错峰更新。 - 多级缓存
引入本地缓存(如 Caffeine、Guava Cache)作为一级缓存,Redis 作为二级缓存。本地缓存失效时,Redis 还能挡住一部分请求。
2. 针对 Redis 实例不可用
- 高可用部署
使用 Redis 主从 + 哨兵(Sentinel)或 Redis Cluster,保证单节点故障时能自动切换。 - 限流 & 熔断
在应用层使用限流组件(如 Sentinel、Hystrix)对数据库访问进行限流,防止大量请求同时击垮数据库。 - 降级
当 Redis 不可用时,返回默认值或静态数据,暂时牺牲一致性保证系统可用性。 - 持久化 + 预热
重启后,利用持久化文件(RDB/AOF)快速恢复缓存,并提前预热热点数据。
总结对比
| 问题 | 原因 | 核心应对 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、缓存空对象、参数校验 |
| 缓存击穿 | 热点 key 过期瞬间高并发 | 互斥锁、逻辑过期、永不过期 |
| 缓存雪崩 | 大量 key 同时过期 / Redis 不可用 | 过期随机、多级缓存、高可用、限流降级 |
在实际项目中,通常会组合使用多种策略来保障缓存层的稳定性,例如对核心热点数据采用“永不过期+异步刷新”,对一般数据采用“过期随机+互斥锁”,并在入口层统一做布隆过滤和参数校验。