校验必须在缓存层之前:对ID用类型转换过滤,字符串key限定长度与字符集;空值缓存需加随机TTL防击穿,布隆过滤器须与DB更新强同步;Key应分离存在性与数据内容。

用户输入校验必须在缓存层之前做
缓存穿透本质是非法或不存在的 key 持续打到后端,所以第一道防线不是 Redis,而是业务入口。如果用户传 id=abc 这种明显非数字 ID,或者空字符串、超长字符串、SQL 片段,根本别让它进缓存查询流程。
- 对 ID 类参数,用正则或类型转换快速过滤:
parseInt(id)后判断是否为有效整数,isNaN()为 true 就直接拒绝 - 对字符串类 key(如用户名),限定长度(如 2–32 字符)、字符集(
/^[a-zA-Z0-9_\-]+$/),不满足就返回 400 - 不要依赖 Redis 返回
null再做判断——那已经穿透了一次,且可能被恶意放大 - Web 框架层(如 Express 中间件、Spring MVC @Valid)做校验比在 service 层更早、更省资源
空值缓存不能只设固定过期时间
把 null 或空对象写进 Redis 是常见手段,但只设 EX 60 很危险:热点空 key 被高频请求时,缓存一过期就集体击穿;冷门空 key 又长期占内存。
- 对明确不存在的 key(如已删除商品 ID),用短 TTL + 随机偏移:
SET goods:999999 "" EX 60 PX 5000,避免雪崩 - 对“暂时查无结果”(比如异步任务未完成),建议用带业务语义的占位值,如
"pending",并配合单独的轻量轮询机制 - 注意:Redis 6.2+ 支持
SET key "" NX EX 60原子写入,老版本要用SETNX+EXPIRE组合,否则有竞态风险 - 空值缓存要和真实数据缓存使用相同序列化方式(如 JSON.stringify),否则反序列化可能出
undefined导致二次穿透
布隆过滤器得和业务更新强绑定
布隆过滤器(Bloom Filter)能高效拦截 99% 以上无效 key,但它本身不自动感知数据变更。如果商品下架了,而布隆过滤器里还标记“存在”,就会误拒合法请求。
- 初始化阶段,用全量 ID 扫描构建初始 filter,加载到 Redis 的
bf.reserve结构(RedisBloom 模块)或本地new BloomFilter() - 每次写 DB 成功后,同步调用
bf.add;删 DB 前,调用bf.remove(如果支持)或清空对应 key + 重建局部 filter - 不推荐纯客户端布隆过滤器(如 Java 的 Guava BloomFilter),因为多实例无法共享状态,容易漏判
- 误判率控制在
0.01以内即可,再低会显著增加内存;用bf.cardinality定期检查容量,超 80% 就触发扩容或重建
RedisKey 设计要区分“存在性”和“数据内容”
很多团队把所有逻辑塞进一个 key,比如 user:123 既存用户数据,又靠它是否存在来判断用户存不存在——这会让空值缓存、布隆过滤器、DB 查询三者耦合,一环出错全崩。
- 拆成两个 key:
user:exists:123(布尔 or 空字符串,用于快速存在性判断),user:data:123(JSON 数据) - 布隆过滤器只校验
user:exists:*前缀的 key,不碰user:data:* - DB 更新时,先更新
user:exists:123,再更新user:data:123;读取时,先查 exists key 或布隆过滤器,再决定是否查 data key - 这个分离让监控更清晰:你可以单独看
user:exists:*的命中率,而不会被 data key 的过期策略干扰
真正难的不是加布隆过滤器,是让它的生命周期和业务数据变更节奏对齐。漏掉一次 bf.add,可能引发一小时的穿透流量;而过度保守地缓存空值,又会拖慢正常请求。这两边的阈值,得靠线上 redis-cli --stat 和业务日志里的 key 分布一起调。


















