单用 SET 或 GET 处理 String 类型缓存必然扛不住缓存穿透,必须组合布隆过滤器(事前拦截)+ 空值缓存(事后兜底)。因为 String 类型无内置校验,对 user:-1、user:abc 等非法 key 一律返回 null,导致请求直击数据库;布隆需预热至应用内存并同步写库,空值须用结构化占位符、短 TTL(如 60 秒)且 key 空间严格一致。

直接说结论:单用 SET 或 GET 处理 String 类型缓存,不加任何前置拦截或兜底策略,必然扛不住缓存穿透。必须组合使用布隆过滤器(事前拦截)+ 空值缓存(事后兜底),缺一不可。
为什么 String 类型特别容易被穿透
String 是 Redis 最基础的类型,业务中大量用于 ID → 对象的直映射(如 user:12345 → JSON)。但它的原子性也带来隐患:没有内置校验、不自动拒绝非法 key、也不区分“查不到”和“根本不可能存在”。一旦前端传入 user:-1、user:abc、user:999999999999 这类明显越界的值,Redis 会老老实实执行 GET,返回 null,然后请求全量涌向数据库。
常见错误现象包括:
-
redis.keyspace_misses持续飙升,而redis.keyspace_hits不涨 - 数据库慢查询日志里反复出现
SELECT ... WHERE id = ?返回空结果 - Redis 内存中突然多出大量
user:*key,但 value 全是空字符串或"null"
布隆过滤器必须在应用层初始化,不能靠 Redis 命令临时建
Redis 6.2+ 虽支持 BF.RESERVE,但生产环境不建议每次请求都走一次网络调用去查 BF.EXISTS——延迟高、连接压力大、还容易误判率失控。真正可靠的做法是把布隆过滤器加载到应用进程内存中,作为单例常驻。
实操要点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 初始化时按业务全量预估:比如用户表上限 5000 万,允许 0.5% 误判率,用
bloom.NewWithEstimates(50000000, 0.005),实际内存约 30MB - 写库必须同步更新布隆:新增用户后立刻调用
bloomFilter.put(userId);删用户无法删除,只能定期重建或配合二级缓存标记逻辑删除 - 查询流程严格为:
bloomFilter.mightContain(id)→ false 则直接返回 404;true 才继续stringRedisTemplate.opsForValue().get(cacheKey) - 别在 Controller 层 new 布隆实例——它不是一次性的工具类,是全局共享的状态组件
空值缓存的 value 和 TTL 必须带业务语义
很多人用 SET user:999999 "" EX 60 就以为完事了,结果下游服务把空字符串当真实数据解析,或者 Redis 内存被无效 key 慢慢吃满。
安全写法要满足三个条件:
- value 统一用可识别的占位结构,例如
{"code":404,"msg":"not found"},避免和业务返回的null字符串混淆 - TTL 必须短且可调:60 秒是起点,高并发场景下可压到 10–30 秒;绝对不要设成 1 小时以上或永不过期
- 空值写入前要再做一次格式校验:比如
id <= 0 || id > 999999999的整数直接拦截,不进缓存也不查 DB - 监控必须跟上:
keys user:*数量 +used_memory_human趋势,发现异常增长立即告警
布隆误判和空值过期之间存在时间窗口,得靠日志对齐
布隆说“可能存在”,但实际 DB 也没数据 → 写空值;空值刚好过期那一秒,又来一个相同 key → 再次穿透。这个窗口无法彻底消除,只能收窄。
关键控制点:
- 布隆误判率别设太低:0.1% 误判率对应 ~10 bits/key,设成 0.001% 内存翻倍但收益极小
- 空值 TTL 和布隆重建周期要错开:比如布隆每天凌晨 reload,空值 TTL 控制在 2–5 分钟,避免集体失效
- 所有空值写入操作必须打日志,字段含
cacheKey、ttlSeconds、source(db miss / format reject),方便定位穿透源头是攻击还是 BUG
最易被忽略的是:布隆过滤器和空值缓存的 key 空间必须完全一致。比如布隆里存的是原始 12345,而缓存 key 是 user:12345,那布隆就完全失效了。

















