大促期间缓存穿透会被高并发流量无限放大,导致本该由Redis扛住的请求直接打到数据库上,引发连接池耗尽甚至雪崩,必须在入口层用参数校验、布隆过滤器或空值缓存提前拦截。
缓存穿透怎么解决?先看大促场景下的真实危害
平时缓存穿透像个小偷,偶尔来一次,数据库打个喷嚏就过去了,但大促不一样,流量是平时的几十倍甚至上百倍,穿透请求也跟着成倍放大。
大促流量如何把穿透变成灾难
想象一下这个场景:零点秒杀刚开始,用户疯狂点击一个不存在的商品ID,这个ID既不在Redis里,也不在MySQL里,每个请求都绕过缓存直达数据库,而数据库每条查询都要走一遍完整的索引扫描,然后返回空结果。
平时每秒几百个穿透请求,MySQL还能勉强应付,大促时每秒几千上万个穿透请求同时涌过来,数据库的连接池瞬间被占满,后面正常的读写请求全部排队等待,更麻烦的是,这些穿透请求往往带着相同的参数,比如同一个伪造的商品ID,数据库的查询缓存(如果还开着)也会被这些无效查询刷爆。
穿透请求的三个主要来源
- 恶意攻击:攻击者故意构造不存在的Key,比如负数ID、超长字符串、随机UUID,专门绕过缓存打数据库
- 业务Bug:前端传参没做校验,比如商品ID传了个空字符串或者格式错误的数值
- 数据删除:某个商品下架后,缓存和数据库都删了,但用户收藏夹里还留着旧链接,点进来就是穿透
业内专家指出,大促期间穿透请求中有相当一部分来自自动化脚本,它们会循环遍历ID,从1试到几百万,专门找那些不存在的记录。
穿透和击穿、雪崩的本质区别
很多人把缓存穿透和缓存击穿搞混,其实它们完全不同:
| 类型 | 触发条件 | 影响范围 | 大促风险 |
|---|---|---|---|
| 穿透 | 查询不存在的数据 | 单个Key反复打库 | 恶意请求可无限放大 |
| 击穿 | 热点Key突然过期 | 单个热点Key打库 | 秒杀商品最容易触发 |
| 雪崩 | 大量Key同时过期 | 大面积缓存失效 | 缓存设置了相同过期时间 |
穿透最隐蔽的地方在于:它不需要等Key过期,也不需要热点Key,只要请求的Key不存在,就能一直穿透下去,大促期间系统日志里全是”查无此数据”的SQL记录,DBA看着数据库QPS飙升却找不到明显的热点,这就是穿透的典型特征。
缓存穿透和击穿区别在哪?大促排查别搞混
如何快速区分穿透和击穿
排查问题的时候,先看Redis的命中率曲线,如果是穿透,命中率会持续偏低,因为大量请求查的Key压根不存在,Redis根本无从命中,如果是击穿,命中率在某个时间点突然掉下去,过一会儿又恢复,因为热点Key过期后,第一个请求打库重建缓存,后续请求又命中Redis了。
再看数据库的SQL类型,穿透的SQL大多是SELECT xxx WHERE id = 不存在的值,而且这个值反复出现,击穿的SQL则是针对某一个特定ID的查询,比如秒杀商品的详情页。
行业共识认为,大促期间穿透和击穿经常同时发生,秒杀商品的热点Key过期导致击穿,与此同时攻击者还在用不存在的ID构造穿透,两者叠加会让数据库承受双倍压力。
大促排查穿透的实操路径
- 查看Redis的
keyspace_hits和keyspace_misses指标,如果misses占比超过较大比例,基本可以断定存在穿透 - 打开MySQL慢查询日志,筛选出执行频率高但返回空结果的SQL
- 用
tcpdump抓包分析请求特征,看是否存在规律性的ID遍历 - 检查应用日志中”缓存未命中”的Key分布,找出高频不存在的Key
缓存穿透解决方案对比:布隆过滤器、空值缓存、参数校验
参数校验最便宜的第一道防线
在业务入口做参数合法性校验,拦截掉明显不合法的请求,比如商品ID必须是正整数且小于某个阈值,用户ID必须是特定格式的UUID。
if (productId == null || productId <= 0 || productId > MAX_PRODUCT_ID) {
return "参数错误";
}
这个方案能挡住大部分恶意攻击,但对”合法但不存在的ID”无能为力,比如商品ID 99999是合法的,但数据库里确实没有这个商品,请求照样穿透。
空值缓存简单直接但要注意细节
把不存在的Key也缓存起来,Value设为null或者一个特殊标记,TTL设置短一些,比如30到60秒。
Object value = redis.get(key);
if (value == null) {
Object dbValue = db.query(key);
if (dbValue == null) {
redis.set(key, NULL_MARKER, 30); // 缓存空值30秒
}
return dbValue;
}
优点是实现简单,缺点有两个:一是大量不存在的Key会占用Redis内存,攻击者可以构造几百万个随机Key,把Redis内存塞满;二是TTL过期后,攻击者重新发起同样的请求,又会穿透一次,所以空值缓存适合业务场景中不存在Key数量有限的情况,不适合应对大规模恶意攻击。
布隆过滤器大促最推荐的方案
布隆过滤器是一个bit数组,用多个哈希函数把Key映射到bit位上,判断一个Key不存在时,一定不存在;判断存在时,可能有误判,这意味着它可以完美拦截穿透请求不存在的Key直接返回,根本不会打到数据库。
实操步骤:
- 在应用启动时,把数据库里所有商品ID加载到布隆过滤器
- 请求进来先查布隆过滤器,如果Key不存在,直接返回空结果
- 如果Key存在,再查Redis,Redis没命中再查数据库
- 新增商品时,同步把新ID加入布隆过滤器
// 使用Redisson的RBloomFilter
RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("productBloom");
bloomFilter.tryInit(1000000L, 0.01); // 预期100万条数据,误判率1%
需要注意布隆过滤器不支持删除操作,商品下架后ID仍然留在过滤器里,但这不是问题最多是让这个Key走到Redis和数据库查一遍,然后发现不存在再缓存空值,不会造成穿透。
三种方案怎么选
| 方案 | 实现成本 | 内存占用 | 拦截效果 | 适用场景 |
|---|---|---|---|---|
| 参数校验 | 极低 | 无 | 拦截非法参数 | 所有系统必备 |
| 空值缓存 | 低 | 较高 | 拦截重复穿透 | 不存在Key有限 |
| 布隆过滤器 | 中 | 较低 | 拦截所有穿透 | 大促高并发场景 |
大促场景下,建议参数校验 + 布隆过滤器 + 空值缓存三层组合,参数校验挡掉明显非法的,布隆过滤器挡掉不存在的,空值缓存兜底处理布隆过滤器误判的那部分请求。
大促前如何验证防御效果
压测模拟穿透流量
写一个压测脚本,随机生成不存在的商品ID,模拟攻击者行为,观察数据库QPS是否稳定,Redis命中率是否正常,压测时重点关注数据库连接池使用率,如果超过80%,说明防御还不够。
监控关键指标
- Redis的
rejected_connections,连接被拒说明压力过大 - MySQL的
Threads_running,超过50说明有堆积 - 慢查询数量,穿透会导致大量空查询变慢
- 布隆过滤器的误判率,通过日志统计实际误判比例
应急预案
即使做了防御,也要准备Plan B,在网关层配置限流规则,针对同一个Key的请求频率做限制;开启降级开关,数据库压力过大时直接返回兜底数据,比如商品已下架的提示页面。
缓存穿透对大促数据库的隐性压力:常见问题解答
大促期间布隆过滤器误判的请求怎么处理?
误判意味着Key实际上不存在,但布隆过滤器说存在,这类请求会继续查Redis和数据库,最终返回空结果,处理方式是:在数据库查询层加一层空值缓存,把误判的Key缓存30秒,避免同一个误判Key反复查库,布隆过滤器误判率通常控制在1%以下,配合空值缓存完全够用。
缓存穿透和缓存雪崩在大促时哪个更危险?
穿透更危险,雪崩是大量Key同时过期,但Key本身是存在的,重建缓存只需要查一次数据库,后续请求又能命中Redis,穿透则不同,每次请求都是无效查询,数据库白白承受压力却不产生任何缓存收益,更关键的是,穿透可以被攻击者利用,用少量请求就能拖垮数据库,而雪崩往往是配置不当造成的偶发问题。
空值缓存的TTL设置多长时间合适?
建议设置在30秒到5分钟之间,TTL太短,比如10秒,攻击者频繁刷新就能绕过缓存继续穿透;TTL太长,比如1小时,大量不存在的Key堆积在Redis里,既浪费内存又可能导致Key淘汰掉正常数据,大促期间可以适当缩短到30秒,因为攻击流量大,宁可多查几次数据库也要控制内存占用。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/772498.html


