空值缓存的原理
缓存穿透指查询一个数据库里根本不存在的键:缓存永远不命中,每次请求都打到数据库。空值缓存把「查不到」这个结果也写进缓存,用一个不会被业务产生的标记值表示,配一个较短的 TTL。后续同样的请求在缓存层就被拦下,数据库压力随之下降。代价是读路径必须统一处理标记值,写路径必须记得清理它。
基本信息
| 适用平台 | 任意 Redis 版本 + 应用侧封装 |
|---|---|
| 关键设计 | 标记值、较短 TTL、统一读取路径、写后清理 |
| TTL 建议 | 空值比正常值短(常见 60-120 秒) |
| 标记值选择 | 业务绝不可能产生的字符串 |
| 配套手段 | 参数校验、布隆过滤器、接口限流 |
典型使用场景
- 按用户 ID 查询详情:大量无效 ID 来自爬虫或攻击
- 商品编号扫描:不存在的编号被反复请求
- 用户注册后立即查询:新建数据与空值标记的衔接
- 对外接口防刷:用空值缓存降低穿透带来的数据库压力
- 与限流结合:对高频无效查询做双层拦截
统一封装示例
统一封装示例
NULL = '__NULL__' # 业务不可能产生的标记值
def cache_get(key, loader, ttl=300, null_ttl=60):
v = redis.get(key)
if v == NULL:
return None # 命中空值标记,直接返回不存在
if v is not None:
return v # 命中正常缓存
val = loader() # 回源查询数据库
if val is None:
redis.set(key, NULL, ex=null_ttl) # 短 TTL 写入空值
else:
redis.set(key, val, ex=ttl)
return val实现要点
| 要点 | 做法 | 忽略的后果 |
|---|---|---|
| 标记值 | 选择业务不会产生的字符串 | 真实数据被误判为不存在 |
| TTL | 空值设置更短的过期时间 | 新建数据后长时间读不到 |
| 读取路径 | 所有读取统一走封装函数 | 部分路径绕开标记判断导致返回错误 |
| 写路径 | 数据创建后删除对应标记键 | 用户注册后仍查不到自己的信息 |
| 兜底 | 配合参数校验或布隆过滤器 | 空值缓存写入量本身成为压力 |
空值缓存占用内存但成本可控:TTL 通常只有一到两分钟,只要标记值与正常值不冲突,它是一个实现简单、收益明显的防穿透手段。
对于大规模随机无效键的攻击,空值缓存会被大量写入从而占满内存:此时应先做参数合法性校验和限流,必要时用布隆过滤器在入口层拦截。