缓存穿透

查询不存在的数据时缓存永不命中,请求持续落到数据库;对策包括空值缓存(短期)、布隆过滤器前置拦截与参数合法性校验。

缓存穿透是什么

缓存穿透指查询的是不存在的数据:缓存里必然没有,请求每次都落到数据库,缓存层等于失效。它和缓存击穿(热点键失效瞬间的并发回源)不同——穿透的根源是“键不存在”,因此查询结果为空时也需要被缓存或提前拦截。常见对策有三层:前置参数校验拦掉明显非法的键、布隆过滤器判断“可能存在”、以及对确认不存在的键缓存一个短期的空值标记。

基本信息

空值缓存对不存在的结果缓存短 TTL,注意与新增数据的一致性
布隆过滤器判断“可能存在”,明显不存在直接拒绝
校验ID 格式与范围校验,拦截明显非法请求
与击穿的区别击穿是热点键失效引发并发回源,穿透是键本身不存在

典型使用场景

  • 恶意扫描防护:攻击者用随机 ID 批量请求,绕过缓存直接打库
  • 用户输入查询:由用户自由输入编号的查询接口容易被构造不存在键
  • 对外开放接口:第三方按 ID 拉取资源,非法 ID 比例不可控
  • 商品或内容详情:下架与不存在的数据被反复请求
  • 权限校验前置:查不到资源时也要尽早返回,避免拖长连接

防护流程(伪代码)

防护流程(伪代码)
v = cache.get(key)
if v == NULL_MARK: return None        # 空值缓存命中,直接返回
if v is None:
    v = db.query(key)
    if v is None: cache.set(key, NULL_MARK, ttl=60s)
    else: cache.set(key, v, ttl=10m)
return v

# 新增记录:写库成功后删除对应空值标记
# 前置校验:ID 格式与范围检查放在查缓存之前

三种对策对照

对策拦截能力主要成本
参数格式校验拦截明显非法的键(如非数字、超长、负数)几乎无成本,但挡不住格式合法的随机值
布隆过滤器拦截确定不存在的键,误判率可配置需维护与数据同步,删除操作支持有限
空值缓存对已查过的空键生效,覆盖随机扫描占用缓存空间,需要与写入流程联动清理

使用要点

  • 空值缓存要与数据创建流程联动:新建记录后主动清理对应空值标记,避免短暂不可见
  • 空值缓存 TTL 要明显短于正常数据的 TTL,控制不一致窗口
  • 布隆过滤器要与数据源同步更新:初始化与增量写入都要覆盖,否则误判会放大
  • 把空值命中率与数据库回源量做成指标,穿透攻击会表现为回源量异常上升
  • 对同一来源的异常键做限流:随机扫描往往来自少数来源,可直接在接入层拦截
空值缓存要与数据创建流程联动:新建记录后主动清理对应空值标记,避免短暂不可见;这个清理动作要写进创建流程,而不是靠过期时间被动恢复。
布隆过滤器只回答“可能存在”或“一定不存在”:删除数据后无法直接移除对应位,需要重建或采用可删除的变体,否则会长期高估存在性。

易错点

  • 恶意随机 ID 持续打库,缓存层形同虚设,数据库在低缓存命中率下被打满
  • 空值缓存未随数据创建清理,新建记录后短期内查询仍然返回不存在
  • 只做格式校验不拦截随机合法值,穿透流量依然全部落到数据库
  • 布隆过滤器未与数据源同步,误判率上升,正常存在的键被错误拒绝
  • 把穿透与击穿混为一谈:按击穿的方案(热点预热)治理穿透,问题依旧存在

数据来源与参考

  • Redis 文档:布隆过滤器(Bloom Filter)
  • 《System Design Interview》卷 1(Alex Xu):缓存章节
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌