防止String缓存穿透需前置校验、空值缓存、布隆过滤器;防击穿须用互斥锁重建热点key,二者手段不可混用,且必须按序部署四层防护。

直接说结论:防止 String 类型缓存穿透,核心是“不让非法或不存在的 Key 触发数据库查询”;防止缓存击穿,关键是“避免热点 String Key 过期瞬间并发重建”。两者不能混用同一套逻辑,参数校验、空值缓存、布隆过滤器、互斥锁这四类手段必须按场景拆开用。
缓存穿透:为什么 String 类型特别容易被穿透
因为 String 是最基础的 Redis 数据类型,业务中大量用于存储用户信息、配置项、短链映射等——这些 key 往往直接拼接 ID(如 "user:123"、"config:pay_timeout"),一旦前端传参不加约束(比如 id=-1、id=999999999999),Redis 查不到就必然走 DB,且永远查不到。没有结构体校验,也没有自动类型过滤,String 的“裸奔”特性放大了穿透风险。
常见错误现象:redisTemplate.opsForValue().get("user:-1") 返回 null,代码没做空值拦截,直接调用 userMapper.selectById(-1),MySQL 日志里全是 WHERE id = -1 这类无效查询。
- 必须前置校验参数合法性(如 ID > 0、符合正则
^\d{1,19}$) - 数据库查空后,写入
"NULL"或空 JSON 字符串(如"{}"),并设短过期(5min足够,别用30min) - 高并发或对外暴露接口时,务必上布隆过滤器——不是可选项,是安全底线;
BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000, 0.01)初始化后,所有查询先过mightContain(key) - 注意:布隆过滤器误判率 ≠ 0,所以仍要保留空值缓存兜底,不能只信它
缓存击穿:String 热点 key 过期时的并发重建陷阱
String 类型击穿比 Hash 或 JSON 更隐蔽——因为它没有字段级更新能力,整个 key 要么全量刷新,要么全量失效。比如首页 banner 配置 "banner:active" 设了 30min 过期,整点一到,所有请求同时发现缓存 miss,全部涌向数据库查同一条记录。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
典型错误写法:if (value == null) { value = loadFromDB(); redis.set(key, value, 30, MINUTES); } —— 这段代码在并发下会触发 N 次 DB 查询。
- 用
SET key value EX 30 NX(即redisTemplate.opsForValue().setIfAbsent())抢锁,抢到的线程去查 DB 并写缓存,其余线程等待后重试读缓存 - 不要用本地锁(
synchronized),集群环境下完全无效;优先用 Redisson 的RLock,或自己封装带自动续期的 Lua 脚本锁 - 对绝对热点
String(如系统开关、活动开关),干脆取消过期时间,改用后台定时任务或消息队列异步刷新,例如每 5 分钟redis.set("feature:login", "true") - 避免把多个逻辑塞进同一个
Stringkey,比如把 10 个开关拼成一个 JSON 字符串再缓存——击穿时整条重建代价太大;应拆成独立 key,各自控制生命周期
为什么空值缓存和互斥锁不能互相替代
空值缓存解决的是“请求是否该继续往下走”的问题,本质是**过滤层**;互斥锁解决的是“谁来重建缓存”的问题,本质是**协调层**。两者作用域完全不同。
错误认知:“我用了空值缓存,就不用加锁了”——错。空值缓存只对 NULL 生效,而击穿发生在真实存在的热点 key 上,它根本不会进入空值分支。
- 空值缓存适用于:查询结果确定为空(如
userMapper.selectById(0)返回 null) - 互斥锁适用于:查询结果存在但缓存已过期(如
banner:active刚过期,DB 能查到最新值) - 生产环境必须同时部署:参数校验 → 布隆过滤器 → 空值缓存 → 互斥锁重建,缺一不可
- 最容易被忽略的一点:布隆过滤器加载时机。不能靠首次查询时懒加载全量 ID,必须在服务启动或数据变更后主动同步,否则冷启动阶段照样穿透

















