缓存穿透在大促期间会把“查不到数据”的请求直接怼到数据库底层,绕过所有缓存保护,形成一条隐蔽但致命的流量暗河,提前用布隆过滤器和空值缓存才能守住系统。
每年大促,技术团队最怕的往往不是流量峰值,而是那些看起来无害的“空请求”,它们不携带恶意特征,也不触发限流告警,却能在几毫秒内把数据库连接池打满,缓存穿透就是这种隐性压力的典型代表,平时流量低时影响微弱,一旦遇到大促的百倍流量,立刻变成数据库的催命符。
缓存穿透是什么意思?一个“查不到”也能压垮库房的怪圈
缓存穿透,简单说就是请求的数据在缓存和数据库里都不存在,正常请求会先查缓存,缓存没有就查数据库,再把结果写回缓存,但穿透请求查一个“压根不存在”的ID,每次都会绕过缓存直达数据库,数据库查不到,也不会写缓存,于是下次同样的请求继续穿透。
从一次大促事故看穿透的隐蔽性
假设你的电商平台在售商品ID是正整数,大促开始瞬间,恶意脚本或用户手动刷一个不存在的商品ID,10086,常规拦截逻辑只校验是否为数字,不校验是否为正数,于是这个请求直接打到数据库,数据库执行一次索引查询,返回空结果,不落缓存,同一秒内相同的ID被刷几千次,数据库就得扛几千次无意义的查询。
更麻烦的是,这些请求会均匀分布在不同空ID上,每个ID只打一次,数据库的查询日志看起来全是正常的“未命中”,没有明显热点,监控视图里数据库QPS却悄悄翻了几倍,大促期间流量基数大,哪怕只有十分之一的请求是穿透请求,数据库压力就凭空多出一大截。
穿透请求如何一步步逼垮数据库
整个过程就像一群人反复敲一家空仓库的门,仓库管理员每开一次门都要走一遍检查流程,但门后什么都没有,正常请求会把“这间是空的”贴个条贴在门口,下次直接看见条子就走开,穿透请求不给贴条子的机会,因为查不到结果,仓库管理员不知道门后是否永远为空,只能每次都亲自检查。
具体到技术链路,数据库在执行“查无此记录”时的开销并不比查到记录低,MySQL需要走B+树索引查找,找到叶子节点确认无数据,再返回空结果,同一SQL并发量超过数据库连接池上限后,新增请求开始排队,等待超时,前端表现为接口变慢,消费者反复重试,重试又变成新的穿透请求,最终形成雪崩。
缓存穿透和缓存击穿的区别,大促时谁更致命
很多人把穿透和击穿混为一谈,但两者有本质区别,缓存击穿是某个热点key在过期瞬间,大量请求同时打到数据库;缓存穿透则从头到尾都是“查无此书”,缓存里永远没有这个key的存在,大促场景下,击穿往往集中在爆款商品,你能通过预热和加锁解决;穿透则像散弹枪打鸟,哪里都挨打,防不胜防。
击穿有目标,穿透无差别
缓存击穿发生时,你至少知道是哪个key有问题,比如某款手机秒杀,缓存里存了库存数据,过期的一瞬间,上万人同时查询,这种问题可以用互斥锁、逻辑过期、热点key永不过期去处理。

缓存穿透没有任何“目标”,攻击者或用户随机拼凑不存在的ID,每次查的key都不一样,你无法预判哪个key需要提前缓存,更麻烦的是,穿透带来的数据库压力会被统计进正常的查询耗时时长里,导致告警阈值被污染,运维人员看到平均耗时升高,却很难定位到是哪些空查询在拖后腿。
为什么穿透在大促时压力倍增
行业共识是,大促期间技术团队会把精力集中在热点key、热库热表上,很少有人会专门为“不存在的数据”做预案,正常流量增加一倍,数据库压力增加一倍;但穿透流量增加一倍,数据库压力可能增加三倍到四倍,原因很简单,正常缓存命中率大约在90%以上,意味着只有不到10%的请求真正落到数据库,穿透请求的缓存命中率是0%,全部压力直接砸向数据库。
大促流量还会放大穿透的“放大效应”,一个商品详情页里通常包含商品信息、库存、价格、评论等多个接口,如果每个接口都因为同一个不存在的商品ID发生穿透,数据库就要被重复击穿多次,接口层的缓存明明都在,却全部失效于空值。
大促缓存穿透解决方案:从拦截到兜底的四道防线
要解决穿透,不能只指望某一种手段,业界通用的方案就像给数据库装四道门,每扇门拦住一部分流量,下面按照从代码到架构的顺序,逐个拆解。
第一道防线:参数校验,把明显非法请求挡在入口
很多穿透请求其实能被最简单的逻辑拦截,比如ID是否大于0、格式是否符合业务规则、长度是否超限,在大促期间,应该对入参加上更严格的约束。
- 商品ID必须为正整数,负数、零直接抛参数异常
- 订单号需要符合校验码算法,不满足的请求直接丢弃
- 用户ID需要判断是否存在,可以从JWT或本地缓存中的用户列表快速判断
参数校验成本最低,但只能防住“弱智”攻击,真正棘手的穿透请求会使用合法范围内的随机ID,比如一个商品ID范围是1-10000,攻击者循环查9000-10000之间的商品,而其中有些商品正好下架或已删除,这种请求完全通过参数校验,必须靠后面的防线。
第二道防线:布隆过滤器,用少量内存识别“绝对不存在”的key
布隆过滤器是处理缓存穿透的标准工具,它的原理是:把数据库里所有存在的ID预先放入一个bitmap,查询请求进来时,先用布隆过滤器判断这个ID是否可能存在,如果过滤器说“不存在”,直接返回空结果,不再查库。
构建过滤器时,可以定期把商品表的主键同步到布隆过滤器里,具体操作步骤如下:
- 在应用启动时,加载全部商品ID到布隆过滤器
- 每新增一个商品,同步更新过滤器的bitmap
- 每删除一个商品,不建议从过滤器删除(因为布隆过滤器不支持删除),可以定期重建

布隆过滤器最怕误判,但误判率可以通过参数控制,业内一般设置误判率在1%左右,内存占用约为几百万个key只需要几十MB,为了进一步降低误判的影响,可以把过滤器放在Redis里,或者嵌入到各个服务节点的本地内存中,从网络层就拦截穿透。
第三道防线:缓存空结果,给“不存在”一个短暂的缓存过期时间
如果布隆过滤器还没覆盖到所有场景,或者你不想维护额外的过滤器,那就用缓存空结果来兜底,当一个ID在数据库里查不到时,把空值或特殊标记写入缓存,并设置一个较短的过期时间,比如60秒。
- 缓存key使用原有key格式,值设置为空字符串或统一的Null标记
- 过期时间建议设置在30秒到5分钟之间,太短容易被打穿,太长会导致数据延迟可见
- 需要防止缓存空值导致的数据不一致,比如商品重新上架后,需要主动删除空值缓存
这种方式实现简单,不需要额外组件,但它对恶意批量攻击的防御效果有限,因为攻击者可以在每秒生成几万个不同的ID,每个ID只查一次,缓存空值依然被不断写入,数据库压力没减多少,Redis却可能塞满垃圾key,所以空值缓存通常和布隆过滤器搭配使用,过滤器负责拦截大量随机ID,空值缓存负责拦截那些“曾经存在但后来被删除”的ID。
第四道防线:接口限流与降级,保住数据库最后的活路
即便有了前面三道防线,大促期间仍可能有漏网之鱼,此时你需要给数据库加一道保险:当数据库压力达到阈值时,自动触发保护机制。
- 在数据库访问层设置每分钟最大查询数,超过后直接拒绝部分读请求
- 针对热点接口配置独立的限流规则,比如商品详情接口单机允许100QPS,超出部分返回默认空数据
- 开启降级开关,不再查询数据库,而是直接返回缓存的兜底数据,哪怕数据已经过期
这道防线并非为了挡住穿透,而是为了在极端情况下保证数据库不死,大促期间,用户体验可以暂时牺牲一部分,但数据库一旦宕机,整个服务就全盘崩了。
如何提前评估缓存穿透对数据库的压力,别再等大促现场排查
很多团队是在大促演练时才发现穿透问题,但那时往往已经来不及了,更合理的做法是在日常开发中就建立监控和压测机制,把穿透风险量化出来。
监控指标:盯住这三个数据
- 缓存未命中率:正常情况下应该在10%以下,如果大促前该指标持续走高,说明很可能存在穿透
- 数据库空查询比例:在数据库慢查询日志或审计日志中,统计查询结果为空的比例,如果超过三成,穿透可能已经比较严重
- 布隆过滤器拦截次数:在过滤器代码中埋点,记录拦截的key数量,通过这个指标判断穿透流量的总量
监控报表里要区分“业务正常未命中”和“异常空查询”,正常未命中通常会在缓存写回后消除,而异常空查询会持续存在,你可以写一个定时脚本,扫描缓存中不存在的热门key,并主动查询一次数据库,把结果记录到日志中,用日志的分布来判断是否存在规律性穿透。

大促前压测:用“坏数据”压出系统底牌
普通压测用的是真实存在的商品ID,测不出穿透效果,正确的做法是把压测数据集中混入10%到20%不存在的随机ID,观察数据库的连接数和QPS变化。
具体操作流程如下:
- 从压测脚本中生成一批随机数字作为商品ID,范围覆盖线上真实ID区间
- 使用JMeter或Locust设置混合比例,比如80%有效ID加上20%无效ID
- 逐步增加并发,记录数据库QPS、CPU使用率、SQL平均耗时
- 对比全有效ID压测的数据库QPS,差值就是穿透带来的额外压力
如果压测中数据库QPS超出预期两倍,说明当前架构的穿透防护是不够的,此时应该优先上线布隆过滤器,而不是单纯扩容数据库,扩容数据库只能暂时缓解压力,但穿透流量依然存在,扩容成本也会随着流量增长水涨船高。
大促期间怎么临时加一道“速效救心丸”
如果大促已经开始了,才发现穿透异常,又来不及改代码部署?可以在中间件层临时处理,使用Redis或Nginx的规则,把可疑的请求特征直接丢弃。
- 如果请求的key是纯数字且连续跳跃,可以封禁一段时间
- 在OpenResty层设置过滤规则,匹配到查询结果为空的URL,直接返回404
- 用Redis计数器记录每分钟每个IP的查询次数,超过阈值就拒绝服务
这些临时策略虽然不够精确,但能在大促窗口期降低数据库压力,大促结束后,还是要回到工程化的方案上去。
Q&A
缓存穿透如何解决?最简单的方案是什么?
最简单且有效的方案是缓存空结果并配合参数校验,当数据库查无记录时,把空值缓存到Redis并设置短过期时间,后续相同请求直接命中缓存,在接口入口检查ID合法性,把负数、超长ID直接拦截,这套组合能解决大部分非恶意的穿透流量,如果面临高频恶意攻击,再引入布隆过滤器。
缓存穿透和缓存雪崩有什么区别?
缓存穿透查询的是不存在的数据,缓存中永远没有对应key,每次请求都打到数据库,缓存雪崩是指缓存中大量key在同一时间段内集中过期,导致原本应当命中缓存的请求同时落到数据库,两者的共同点是都会让数据库压力骤增,但解决方向不同:穿透靠拦截空值,雪崩靠失效时间随机化和多级缓存。
大促时发现缓存穿透问题,应该先扩容还是先修代码?
先扩容数据库,同时立刻上线临时拦截规则,双管齐下,扩容能快速增加数据库承受力,给代码修复争取时间,临时拦截规则可以通过配置中心或Redis脚本实现,不需要发布代码,等大促结束后,再正式实施布隆过滤器和空值缓存方案,切忌在大促期间临时重构缓存逻辑,改动越大,风险越高。