引言
在高并发场景下,Redis 作为缓存层已经成为标准配置。然而,当缓存与数据库配合不当时,容易出现缓存穿透、缓存击穿和缓存雪崩三大经典问题。本文将深入剖析这三个问题的成因,并给出切实可行的解决方案。
一、缓存穿透(Cache Penetration)
问题描述
缓存穿透是指查询一个不存在的数据。由于缓存中不会有这个结果,请求会直接打到数据库上。如果有大量这样的请求,数据库压力会急剧上升,甚至导致宕机。
典型场景:恶意攻击者构造大量不存在的 ID 进行查询。
解决方案
1. 布隆过滤器(Bloom Filter)
在缓存之前加一层布隆过滤器,用于快速判断一个元素是否可能存在于集合中。如果布隆过滤器判断不存在,直接返回,避免查询数据库。
// 使用 RedisBloom 模块
BF.ADD redis:user:filter 10086
BF.EXISTS redis:user:filter 10086 // 返回 1
BF.EXISTS redis:user:filter 99999 // 返回 0,直接拦截
2. 缓存空值
当数据库查询结果为空时,也将这个空结果缓存起来,但设置一个较短的过期时间(如 60 秒)。
if ($user === null) {
Redis::setex("user:{$id}", 60, 'NULL');
}
二、缓存击穿(Cache Breakdown)
问题描述
缓存击穿是指一个热点 key在缓存过期的瞬间,有大量并发请求同时访问这个 key。由于缓存已失效,所有请求都会打到数据库上。
典型场景:微博热搜、电商大促时的爆款商品信息。
解决方案
1. 互斥锁(Mutex)
当缓存失效时,只有一个线程能去查询数据库并重建缓存,其他线程等待。
$lockKey = "lock:{$key}";
$locked = Redis::setnx($lockKey, 1);
if ($locked) {
Redis::expire($lockKey, 10);
$data = DB::table('users')->find($id);
Redis::setex($key, 3600, serialize($data));
Redis::del($lockKey);
}
2. 逻辑过期(永不过期)
不给热点 key 设置物理过期时间,而是在 value 中存储逻辑过期时间。发现过期时,后台异步重建缓存,前台返回旧数据。
[
"data" => $user,
"expire_at" => time() + 3600
]
三、缓存雪崩(Cache Avalanche)
问题描述
缓存雪崩是指在某一个时刻,大量缓存 key 同时过期,或者 Redis 集群整体故障,导致所有请求直接打到数据库上。
典型场景:批量导入数据时设置了相同的过期时间;Redis 节点宕机。
解决方案
1. 过期时间加随机值
在设置缓存过期时间时,加上一个随机值,避免大量 key 同时失效。
$ttl = 3600 + rand(0, 300); // 基础过期时间 + 0~5分钟随机值
Redis::setex($key, $ttl, $value);
2. 多级缓存
构建本地缓存(如 Laravel File/Array Cache)+ Redis 分布式缓存的两级架构。当 Redis 不可用时,本地缓存仍能承接部分流量。
3. Redis 高可用架构
- 主从复制:读写分离,从节点提供读服务
- Sentinel 哨兵:自动故障转移,主节点宕机时自动选举新主节点
- Cluster 集群:数据分片存储,提升整体可用性和容量
4. 服务熔断与限流
当数据库压力过大时,启动熔断机制,直接返回降级数据或友好提示,保护数据库不被拖垮。
四、总结对比
| 问题 | 成因 | 核心解决思路 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、缓存空值 |
| 缓存击穿 | 热点 key 过期瞬间并发 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 key 同时过期 / Redis 故障 | 随机 TTL、多级缓存、高可用架构 |
在实际项目中,这三种问题往往同时存在,需要根据业务场景组合使用上述方案。合理的缓存策略不仅能提升系统性能,更是保障系统稳定性的重要防线。