Redis缓存穿透指查询既不在缓存也不在数据库中的数据,导致请求直击数据库;解决方案包括接口参数校验、缓存空对象(设短TTL并用特殊标记值)、布隆过滤器(存在误判需组合使用)三道防线。

Redis 缓存穿透本身不会导致数据库索引失效——索引是数据库层面的物理结构,不受缓存层请求是否存在影响。所谓“索引失效”在这里是误判,真实问题是:大量穿透请求执行了 SELECT * FROM table WHERE id = ? 这类无结果查询,而数据库在面对高频、重复的「不存在」条件时,可能因缺乏有效索引或统计信息陈旧,导致执行计划退化(例如全表扫描),进而拖慢响应。解决重点不在“修复索引”,而在**切断无效查询路径 + 优化数据库对空查的响应效率**。
为什么“id = -1”这类查询会让数据库变慢?
MySQL/PostgreSQL 对 WHERE id = ? 查询是否走索引,取决于几个关键事实:
- 该字段是否有有效索引(如
PRIMARY KEY或UNIQUE INDEX)——有则基本能走索引; - 查询值是否被优化器识别为“不可能存在”(例如
id = -1且id是UNSIGNED INT)——此时会快速返回空集; - 但若字段是
VARCHAR、或 ID 范围宽泛(如 1~10⁹)、或统计信息过期,优化器可能放弃索引,选择全表扫描来确认“真不存在”; - 更隐蔽的是:某些 ORM(如 MyBatis 的
selectOne)在查不到时仍会执行完整 SQL,不带LIMIT 1,加剧开销。
缓存空对象时,如何避免覆盖真实数据?
缓存空值不是简单地 SET key "" EX 300,必须区分“空”和“未初始化”。否则后续写入真实数据时,可能被旧的空缓存遮蔽:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用显式标记值,比如
SET key "@@NULL@@" EX 300,而非null或空字符串,避免与业务中合法的空字符串混淆; - 读取时严格判断:
if (cacheVal != null && "@@NULL@@" .equals(cacheVal)) { return null; }; - 写入真实数据前,必须先
DEL key,再SET key real_value EX 3600,不能依赖覆盖——Redis 的SET虽然会覆盖,但若并发中先写空、再写真实值,中间窗口期仍可能返回空; - 推荐加一层原子操作封装,例如 Lua 脚本:
EVAL "if redis.call('GET', KEYS[1]) == '@@NULL@@' then redis.call('DEL', KEYS[1]) end; redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])" 1 mykey "real_data" 3600。
布隆过滤器上线后,为什么还有穿透?
布隆过滤器(Bloom Filter)不是万能的,它只保证「存在性」的**负向准确**:如果 bloom.mightContain(id) 返回 false,那该 id 一定不存在;但如果返回 true,只是“可能存在”——仍有误判率(通常设为 0.1%~1%)。这意味着:
- 攻击者可利用误判率,持续发送布隆判定为“可能存在的”非法 ID(如随机生成的 128 位字符串),绕过第一道过滤;
- 布隆数据需与数据库一致:若新增用户后未及时
bloom.add(userId),会导致合法请求被拦截(漏判),必须配合 Binlog 监听或消息队列同步; - 布隆本身不处理参数格式问题——
id="abc"这种非数字字符串,布隆无法校验,必须前置做类型解析和基础校验(如正则^\d+$); - 线上建议组合使用:
参数校验 → 布隆过滤 → 缓存空值,三层漏斗,越往前拦截成本越低。
真正容易被忽略的点是:缓存空值的 TTL 不应机械设为“5 分钟”,而要结合业务容忍度与数据变更频率。例如用户资料删除后,若允许最多 5 分钟内查到“已删除”状态,就设 300 秒;若要求实时,就得用布隆 + 事件驱动清理,而不是靠过期被动等待。

















