枪战英雄
99.99M · 2026-03-28
缓存穿透一般指:请求所带的标识(如用户 ID)在缓存里查不到,于是落到数据库;而数据库里同样不存在这条数据,因此也无法回写一条「有意义的缓存记录」。结果是:同一类恶意或异常请求会反复穿透缓存,每次都打数据库。
若这类请求量很大,数据库可能在短时间内承受远超平时的 QPS,存在被打垮的风险。
对入参做业务规则校验,从源头减少「注定查不到」的请求。
例如:合法用户 ID 约定为 15xxxxxx 形态,则对 16232323 这类明显不符合规则的 ID 可直接返回错误,不再访问缓存与数据库。这能过滤一部分伪造或扫库的恶意请求。
原理简述:底层用 bit 数组表示集合;初始化时把数据库中已存在的 key 经多次哈希(如三次)映射到多个下标,并将对应位置置为 1。查询时同样做哈希,若相关位不全为 1,则可判定「一定不存在」,从而避免无意义的数据库查询。
能解决:大量「确实不存在」的请求在过滤器层被挡掉,减轻数据库压力。
需注意的两点:
| 问题 | 说明 |
|---|---|
| 误判 | 哈希存在冲突,不同 key 可能映射到相同位置,存在「假阳性」——过滤器认为可能存在,实际库中仍没有。通常可通过调整位数组大小与哈希次数权衡。 |
| 数据更新与一致性 | 布隆过滤器与数据库是两套数据源。例如库中新增了用户,若同步到布隆过滤器失败(网络、任务延迟等),可能出现:合法用户被误判为不存在而遭拦截。因此要有可靠的增量同步、补偿或降级策略。 |
当缓存未命中且数据库也查不到时,仍将该 key 写入缓存,值为空或占位(可配合较短 TTL,避免长期占用内存)。
后续相同 key 的请求可直接在缓存层得到「空结果」,不再重复查库。
缓存击穿多指:热点 key 在某一时刻过期失效,此时大量并发请求同时未命中缓存,一齐涌向数据库,造成瞬时压力骤增,甚至拖垮数据库。
与「穿透」的区别:击穿场景下,数据在库里一般是存在的,只是缓存这一层暂时失效。
压力来自「同一时刻过多请求同时打库」。可对同一个热点 key(如同一个 productId)加锁:同一时刻只允许一个线程/请求去查库并回写缓存,其余请求短暂等待后读缓存或重试。
击穿与 key 物理过期强相关。可在过期前主动刷新:例如定时任务每隔 20 分钟重建缓存并把 TTL 重新设为 30 分钟,使热点数据在业务高峰期内始终有效。
对数量可控的热点(如秒杀商品 ID),可不设置 Redis TTL,在活动前预热写入缓存,活动结束后手动删除无用 key,从根本上避免「到期瞬间集体失效」。
思路:Redis 中的 key 不设或使用很长的物理 TTL,在 value 内携带逻辑过期时间戳;读取时若判断已逻辑过期,则异步刷新缓存,当前请求仍返回旧数据(业务可接受短暂旧读的前提下),避免大量线程同时阻塞在数据库上。
缓存实体示例:
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 表达业务过期):
@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);
}
读取:逻辑过期则异步更新,仍返回旧值:
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();
}
可理解为缓存击穿在规模上的放大:
常见两类场景:
在基准 TTL 上增加随机秒数(如 1~60 秒),从而打散大量 key 的失效时间点,降低同一瞬时打库的概率。
在应用侧维护全局或按资源的降级开关:例如监测到「最近一分钟内 Redis 连续失败达到阈值」,则打开降级,后续请求返回默认值、静态页或简化数据,避免把数据库拖死。
可与配置中心、限流、熔断组件结合使用。