缓存穿透是什么
缓存穿透指查询的是不存在的数据:缓存里必然没有,请求每次都落到数据库,缓存层等于失效。它和缓存击穿(热点键失效瞬间的并发回源)不同——穿透的根源是“键不存在”,因此查询结果为空时也需要被缓存或提前拦截。常见对策有三层:前置参数校验拦掉明显非法的键、布隆过滤器判断“可能存在”、以及对确认不存在的键缓存一个短期的空值标记。
基本信息
| 空值缓存 | 对不存在的结果缓存短 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,控制不一致窗口
- 布隆过滤器要与数据源同步更新:初始化与增量写入都要覆盖,否则误判会放大
- 把空值命中率与数据库回源量做成指标,穿透攻击会表现为回源量异常上升
- 对同一来源的异常键做限流:随机扫描往往来自少数来源,可直接在接入层拦截
空值缓存要与数据创建流程联动:新建记录后主动清理对应空值标记,避免短暂不可见;这个清理动作要写进创建流程,而不是靠过期时间被动恢复。
布隆过滤器只回答“可能存在”或“一定不存在”:删除数据后无法直接移除对应位,需要重建或采用可删除的变体,否则会长期高估存在性。