- 背景和价值
- 一、核心方案1:拦截无效请求——从源头过滤不存在的Key
- 1. 布隆过滤器(Bloom Filter):高效拦截不存在的Key
- 2. 业务规则校验:过滤明显无效的请求
- 二、核心方案2:缓存空数据——避免相同无效请求重复穿透
- 1. 缓存空值+短期过期时间
- 2. 缓存“不存在标记”+ 动态过期
- 三、核心方案3:增强后端防护——极端情况兜底
- 1. 接口限流:控制无效请求总量
- 2. 数据库熔断:保护数据库不宕机
- 3. 数据库层面防护:减少无效查询消耗
- 四、方案选型建议:按场景组合防御
- 总结
- 一、核心方案1:拦截无效请求——从源头过滤不存在的Key
- 参考资料
背景和价值
在Redis缓存架构中,缓存穿透指“请求查询的数据在缓存(Redis)和数据库中均不存在(如查询无效ID、恶意伪造Key),导致所有请求直接穿透缓存冲击数据库,引发数据库压力过载甚至宕机”的问题。与“缓存击穿(热点Key失效)”不同,穿透的核心是“数据本身不存在”,解决逻辑需围绕“拦截无效请求、避免空数据穿透、增强后端防护”展开,具体落地方案如下:
一、核心方案1:拦截无效请求——从源头过滤不存在的Key
通过“概率性数据结构”或“业务规则校验”,在请求进入缓存/数据库前拦截无效Key,避免无效请求消耗后端资源。
1. 布隆过滤器(Bloom Filter):高效拦截不存在的Key
这是解决缓存穿透最主流、最高效的方案,核心是用“空间紧凑的概率性结构”提前存储“数据库中所有有效Key”,快速判断请求Key是否可能存在。
- 核心原理:
布隆过滤器通过多个哈希函数将有效Key映射到一个二进制数组(BitArray)中,标记为“1”;当请求Key到来时,通过相同哈希函数计算映射位置,若所有位置均为“1”,则Key“可能存在”(允许极小误判);若任一位置为“0”,则Key“绝对不存在”,直接拦截。 - 实现流程:
- 初始化:数据库写入新数据时(如新增商品、用户),同步将Key(如商品ID)插入布隆过滤器;
- 请求拦截:请求先经过布隆过滤器校验——
- 若判定“不存在”:直接返回空结果(如“数据不存在”),不访问Redis和数据库;
- 若判定“可能存在”:再查询Redis,Redis无数据则查数据库(数据库也无则走后续空值缓存逻辑)。
- 关键参数优化:
- 误判率:默认控制在0.1%~1%(误判会导致少量无效请求穿透,但可接受),误判率越低需越多哈希函数和Bit数组空间;
- 动态扩容/重建:若数据库Key总量增长,需定期重建布隆过滤器(避免Bit数组拥挤导致误判率飙升),可通过“双过滤器切换”(新过滤器构建完成前用旧过滤器,避免拦截中断)实现。
- 优势:时间复杂度O(1),空间效率极高(存储1亿个Key仅需约12MB);
- 注意:布隆过滤器不支持删除操作(删除会影响其他Key的判断),需通过“定期重建”解决数据删除后的准确性问题。
2. 业务规则校验:过滤明显无效的请求
结合业务场景的“Key格式、范围、合法性”提前拦截无效请求,适合有明确业务规则的场景。
- 实现逻辑:
- 格式校验:如用户ID为10位数字,拦截“字母+数字”“少于10位”的ID请求;订单号为“OD+16位数字”,拦截不符合格式的请求;
- 范围校验:如商品分类ID仅1-100,拦截“分类ID=101”的请求;用户年龄查询范围0-150,拦截“年龄=200”的请求;
- 白名单校验:核心业务接口(如支付、核心商品查询)可维护“有效Key白名单”,仅允许白名单内的Key进入后续链路(适合Key总量少且固定的场景)。
- 优势:实现简单(无需额外组件),无误判,适合业务规则明确的场景;
- 局限:无法覆盖“格式合法但实际不存在”的Key(如10位数字的无效用户ID),需配合其他方案使用。
二、核心方案2:缓存空数据——避免相同无效请求重复穿透
对于“格式合法但数据库不存在”的Key(如10位有效格式的无效用户ID),通过“缓存空值”让后续相同请求从缓存获取结果,不再穿透数据库。
1. 缓存空值+短期过期时间
- 核心逻辑:当请求查询的Key在缓存和数据库中均不存在时,不直接返回空,而是向Redis写入“空值(如""、null)+ 短期过期时间(如30秒~5分钟)”,后续相同请求从Redis获取空值,无需访问数据库。
- 实现流程:
- 请求查询Key→Redis无数据→查询数据库;
- 数据库返回“无此数据”→向Redis写入
SET key "" EX 30
(30秒过期); - 30秒内再次查询该Key→Redis返回空值,直接响应,不访问数据库;
- 30秒后Key自动失效,若仍有请求则重新触发“查库→缓存空值”逻辑(避免长期缓存无效数据占用空间)。
- 关键优化:
- 过期时间:根据业务场景设置(如高频无效请求设5分钟,低频设30秒),避免空值缓存占用过多Redis空间;
- 区分“真空”与“缓存空值”:可在Redis中用特殊标记存储空值(如
SET key "NULL_MARKER" EX 30
),避免与业务中的“合法空数据”(如用户未填写的地址)混淆。
- 优势:实现简单,能有效拦截“相同无效Key的重复请求”;
- 注意:需防范“恶意批量伪造不同无效Key”(如每秒请求1000个不同的无效用户ID),此时空值缓存会占用大量Redis空间,需配合布隆过滤器或限流方案。
2. 缓存“不存在标记”+ 动态过期
针对“长期不存在的Key”(如已删除的用户ID、下架的商品ID),可缓存“永久不存在标记”并定期清理,减少重复查库。
- 实现逻辑:
- 数据库确认数据永久删除(如用户注销、商品彻底下架)后,向Redis写入“永久不存在标记”(如
SET key "PERMANENT_NULL" EX 86400
,设置1天过期,每天凌晨通过定时任务重新校验数据库,确认仍不存在则续期); - 后续请求查询该Key时,Redis返回“PERMANENT_NULL”,直接响应“数据已删除”,无需查库。
- 数据库确认数据永久删除(如用户注销、商品彻底下架)后,向Redis写入“永久不存在标记”(如
- 优势:适合“数据明确永久不存在”的场景,减少长期无效请求;
- 注意:需确保“永久不存在标记”能被定期校验,避免数据库数据恢复后缓存仍返回无效标记。
三、核心方案3:增强后端防护——极端情况兜底
当上述方案失效(如布隆过滤器误判、恶意批量伪造新无效Key)时,通过“限流、熔断、数据库防护”等手段,避免数据库被压垮。
1. 接口限流:控制无效请求总量
基于接口层面设置QPS阈值,限制单位时间内的请求数量,避免突发大量无效请求冲击数据库。
- 实现方式:
- 使用限流组件(如Sentinel、Redis的
INCR + EXPIRE
实现简单限流),对“查询类接口”设置限流规则(如每秒最多1000次请求); - 针对“疑似穿透请求”(如短时间内大量不同的Key请求),可动态降低限流阈值(如从1000降至500),优先保障正常请求。
- 使用限流组件(如Sentinel、Redis的
- 示例:用Redis实现限流——
每次请求前执行INCR limit:api:query
,若结果>1000(QPS阈值),则返回“服务繁忙”;同时执行EXPIRE limit:api:query 1
(1秒过期),确保每秒重置计数。
2. 数据库熔断:保护数据库不宕机
当数据库压力达到阈值(如CPU使用率>80%、连接数>最大连接数的90%)时,触发熔断机制,暂时停止接收非核心请求,仅允许核心请求访问。
- 实现逻辑:
- 用熔断组件(如Resilience4j、Sentinel)监控数据库指标,当指标触发阈值时,熔断“非核心查询接口”(如商品详情查询),返回“服务暂时不可用”;
- 熔断后定期探测数据库状态(如每10秒尝试一次查询),若数据库恢复正常则关闭熔断。
3. 数据库层面防护:减少无效查询消耗
即使有少量请求穿透到数据库,也可通过数据库优化减少资源消耗:
- 索引优化:为查询字段建立索引(如用户ID、商品ID),避免无效查询触发全表扫描;
- 查询限制:对“不存在数据”的查询设置快速返回逻辑(如数据库存储过程中先判断Key是否存在,不存在则立即返回);
- 只读副本:将查询请求路由到数据库只读副本,避免冲击主库(适合读多写少场景)。
四、方案选型建议:按场景组合防御
场景类型 | 推荐方案组合 | 典型案例 |
---|---|---|
大量无效Key+明确有效Key | 布隆过滤器 + 缓存空值 + 接口限流 | 用户ID查询、订单号查询 |
业务规则明确的请求 | 业务规则校验 + 缓存空值 | 商品分类查询、年龄范围查询 |
恶意批量伪造新无效Key | 布隆过滤器 + 接口限流 + 数据库熔断 | 爬虫攻击、恶意请求 |
数据永久不存在(如删除) | 缓存“永久不存在标记” + 定期校验 | 注销用户查询、下架商品查询 |
总结
解决Redis缓存穿透的核心是“多层拦截、层层递进”:先用布隆过滤器/业务规则拦截大部分无效请求,再用“缓存空值”拦截相同无效请求的重复穿透,最后用限流/熔断保护后端数据库。实际落地中需注意:
- 避免过度设计:如非高频无效请求场景,仅需“缓存空值”即可,无需引入布隆过滤器;
- 平衡资源消耗:布隆过滤器需控制误判率和空间占用,空值缓存需设置合理过期时间,避免浪费Redis资源;
- 动态调整策略:根据请求特征(如无效Key类型、流量峰值)定期优化方案(如调整布隆过滤器参数、限流阈值)。