缓存的难点不是 GET,而是缓存失效、异常流量和回源压力叠加时,系统还能不能维持服务。穿透、击穿、雪崩看起来都是"缓存没命中",根因却不同,治理手段也不能混用。
目录
一、先定义缓存命中链路

典型 Cache Aside 链路是:查缓存 → 未命中查数据库 → 回填缓存 → 返回。任何一步超时都要有边界,不能让缓存层把数据库连接池拖垮。
二、缓存穿透
穿透是大量请求查询根本不存在的 Key,每次都绕过缓存打到数据库。攻击者可以随机生成商品 ID,普通业务流量也可能因参数校验缺失造成穿透。
第一道防线是参数校验;第二道是空值缓存,给不存在结果设置较短 TTL;第三道是布隆过滤器等存在性过滤。空值缓存要防止把后来创建的商品长期挡住,布隆过滤器的误判只能让请求继续查库,不能用于肯定存在。
三、缓存击穿
击穿是某个极热 Key 恰好过期,瞬间大量请求同时回源。互斥锁可以让一个请求重建,其余请求短暂等待;逻辑过期则允许返回旧值,同时异步刷新。
锁的等待时间、重建超时和失败兜底必须明确。不能只写 SET NX,却没有锁拥有者校验和异常释放。
四、缓存雪崩
雪崩是大量 Key 同时失效,或 Redis 集群整体不可用,造成回源洪峰。TTL 加随机抖动、分批预热、多级缓存和限流可以降低峰值,但不能替代容量评估。

五、限流、降级与恢复
当数据库连接池、线程池或下游接口到达阈值时,应优先返回静态兜底、旧缓存或明确的"稍后重试",而不是无限排队。对重建任务使用单飞(single flight)和任务队列,避免每个请求都启动刷新。
六、监控与演练
关注命中率、回源 QPS、空值比例、热点 Key、重建耗时、Redis 延迟和数据库连接池。演练单 Key 过期、批量过期、缓存集群不可用三类场景,分别验证击穿和雪崩策略。
6.1 场景:不存在的商品 ID 把数据库打满
营销活动的落地页接受 productId 参数。攻击流量不断请求随机的超大 ID,Redis 中没有缓存,MySQL 索引查询虽然单次很快,却以数万 QPS 持续消耗连接池。此时命中率下降只是表象,真正的信号是"缓存未命中率高且数据库结果为空的比例异常"。
第一步在网关和接口层校验 ID 格式、范围和签名,明显非法的请求不应进入缓存。对合法格式但不存在的 ID,缓存一个显式空对象,例如 {"found":false},TTL 可以比正常商品详情短并加入随机抖动。不要用空字符串和"缓存不存在"混淆,否则排查时无法判断是缓存 miss 还是负缓存命中。
商品后台新建了原先不存在的 ID 时,要删除这条空缓存,或者用消息事件驱动失效。布隆过滤器适合商品主数据量大且变更相对少的场景:它能快速否定"不存在",但有误判,误判时仍应让请求继续经过正常的查库和回填链路。
6.2 场景:明星商品过期,一秒钟打出几万次回源
product:detail:1001 在直播间是绝对热点。它到期的瞬间,20 000 个请求发现 miss,同时去查库并回填。只加一个互斥锁还不够:如果持锁线程的数据库查询超时,等待线程全部堆积,锁释放后又会形成第二波回源。
适合详情展示类数据的方案是逻辑过期:缓存内容带 expireAt,过期后第一个请求提交异步刷新任务,其余请求返回可接受的旧值。适合必须看到新库存的场景则用 single-flight,同一 Key 只有一个重建任务,其余请求等待一个很短的上限,超过上限返回降级页或库存繁忙。
无论哪种方案,都要限制重建任务队列。刷新失败时保留旧值并做退避,不能立即把 Key 删除后让所有请求回源。热点 Key 的 TTL 也不宜在固定整点统一失效。
6.3 场景:发布后 300 万 Key 同时失效
一次预热脚本把所有详情 Key 都设成固定一小时 TTL,整点同时过期。Redis 并没有故障,但数据库 QPS 暴涨、线程池排队,最终引发级联超时。这是典型雪崩,不是 Redis "扛不住"。
修复包括 TTL 基值加随机窗口、按批次预热、给回源接口做并发阈值与熔断、在 CDN/本地缓存放静态兜底。恢复时不要一次性清缓存或全量预热,应按热度分批回填,并用数据库连接池水位决定下一批速度。
6.4 降级不是返回错误对象那么简单
库存接口不能因为 Redis 超时就默认"库存充足",否则会把风险转成超卖。可以按业务重要性分层:商品描述返回最近一次成功缓存,推荐列表返回空列表,库存和支付直接失败并引导稍后重试。降级响应要带上来源和时间,便于客户端避免把旧数据当成实时数据。
恢复阶段先恢复 Redis 可用性,再逐步放开回源流量。设置一个"回源闸门":只有当数据库连接池、线程池和下游 P99 都低于阈值时,才提高每秒允许的重建任务数。否则缓存刚恢复,预热任务又会把数据库打垮。
记住:穿透治理"无效 Key",击穿治理"单个热点同时回源",雪崩治理"很多 Key 或整层同时失效"。