一、缓存穿透(Cache Penetration)
1.1 是什么
缓存穿透 一般指:请求所带的标识(如用户 ID)在缓存里查不到 ,于是落到数据库 ;而数据库里同样不存在 这条数据,因此也无法回写 一条「有意义的缓存记录」。结果是:同一类恶意或异常请求会反复穿透缓存,每次都打数据库。
若这类请求量很大,数据库可能在短时间内承受远超平时的 QPS,存在被打垮的风险。
1.2 应对思路
(1)参数校验,尽早拦截非法请求
对入参做业务规则校验,从源头减少「注定查不到」的请求。
例如:合法用户 ID 约定为 15xxxxxx 形态,则对 16232323 这类明显不符合规则的 ID 可直接返回错误,不再访问缓存与数据库。这能过滤一部分伪造或扫库的恶意请求。
(2)布隆过滤器(Bloom Filter)
原理简述 :底层用 bit 数组 表示集合;初始化时把数据库中已存在的 key 经多次哈希(如三次)映射到多个下标,并将对应位置置为 1。查询时同样做哈希,若相关位不全为 1,则可判定「一定不存在」,从而避免无意义的数据库查询。
能解决:大量「确实不存在」的请求在过滤器层被挡掉,减轻数据库压力。
需注意的两点:
| 问题 | 说明 |
|---|---|
| 误判 | 哈希存在冲突,不同 key 可能映射到相同位置,存在「假阳性」------过滤器认为可能存在,实际库中仍没有。通常可通过调整位数组大小与哈希次数权衡。 |
| 数据更新与一致性 | 布隆过滤器与数据库是两套数据源 。例如库中新增 了用户,若同步到布隆过滤器失败(网络、任务延迟等),可能出现:合法用户被误判为不存在而遭拦截。因此要有可靠的增量同步、补偿或降级策略。 |
(3)缓存空值(Cache Null)
当缓存未命中且数据库也查不到时,仍将该 key 写入缓存 ,值为空或占位(可配合较短 TTL,避免长期占用内存)。
后续相同 key 的请求可直接在缓存层得到「空结果」,不再重复查库。
二、缓存击穿(Cache Breakdown)
2.1 是什么
缓存击穿 多指:热点 key 在某一时刻过期失效 ,此时大量并发请求同时未命中缓存,一齐涌向数据库,造成瞬时压力骤增,甚至拖垮数据库。
与「穿透」的区别:击穿场景下,数据在库里一般是存在的,只是缓存这一层暂时失效。
2.2 应对思路
(1)互斥锁 / 单飞(Single Flight)
压力来自「同一时刻过多请求同时打库」。可对同一个热点 key (如同一个 productId)加锁:同一时刻只允许一个线程/请求去查库并回写缓存,其余请求短暂等待后读缓存或重试。
(2)自动续期
击穿与 key 物理过期 强相关。可在过期前主动刷新:例如定时任务每隔 20 分钟重建缓存并把 TTL 重新设为 30 分钟,使热点数据在业务高峰期内始终有效。
(3)热点 key 不设物理过期 + 预热
对数量可控 的热点(如秒杀商品 ID),可不设置 Redis TTL ,在活动前预热 写入缓存,活动结束后手动删除无用 key,从根本上避免「到期瞬间集体失效」。
(4)逻辑过期时间
思路 :Redis 中的 key 不设或使用很长的物理 TTL ,在 value 内 携带逻辑过期时间戳 ;读取时若判断已逻辑过期,则异步刷新 缓存,当前请求仍返回旧数据(业务可接受短暂旧读的前提下),避免大量线程同时阻塞在数据库上。
缓存实体示例:
java
public class CacheData<T> {
private T value; // 实际数据
private long expireTime; // 逻辑过期时间戳(毫秒)
public CacheData(T value, long expireSeconds) {
this.value = value;
this.expireTime = System.currentTimeMillis() + expireSeconds * 1000;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
public T getValue() { return value; }
public long getExpireTime() { return expireTime; }
}
写入缓存(不依赖 Redis TTL 表达业务过期):
java
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
public void setCache(String key, String value, long expireSeconds) throws Exception {
CacheData<String> cacheData = new CacheData<>(value, expireSeconds);
String json = objectMapper.writeValueAsString(cacheData);
// 不设置 TTL,或仅作兜底;业务过期由 expireTime 控制
redisTemplate.opsForValue().set(key, json);
}
读取:逻辑过期则异步更新,仍返回旧值:
java
public String getCache(String key) throws Exception {
String json = redisTemplate.opsForValue().get(key);
if (json == null) {
return null;
}
CacheData<String> data = objectMapper.readValue(json,
new TypeReference<CacheData<String>>() {});
if (data.isExpired()) {
asyncUpdate(key); // 异步重建缓存,避免同步打满数据库
}
return data.getValue();
}
三、缓存雪崩(Cache Avalanche)
3.1 是什么
可理解为缓存击穿在规模上的放大:
- 击穿 :往往聚焦在单个热点 key 失效后的并发打库。
- 雪崩 :大量 key 在同一时间段失效 ,或缓存集群整体不可用,导致请求集中落到数据库或下游,风险更大。
常见两类场景:
- 批量 key 同时过期:例如同一批缓存使用了相同或接近的 TTL,到期时刻重叠。
- 缓存服务故障 :单机故障、集群脑裂、机房网络问题等导致整层缓存不可用,所有读请求穿透到数据库。
3.2 应对思路
(1)过期时间加随机偏移
在基准 TTL 上增加随机秒数 (如 1~60 秒),从而打散大量 key 的失效时间点,降低同一瞬时打库的概率。
(2)服务降级与熔断
在应用侧维护全局或按资源的降级开关 :例如监测到「最近一分钟内 Redis 连续失败达到阈值」,则打开降级,后续请求返回默认值、静态页或简化数据,避免把数据库拖死。
可与配置中心、限流、熔断组件结合使用。