缓存先崩了?高并发穿透与雪崩治理手记
背景与问题
2026年第三季度,我负责的会员商品中心经历了一次典型的高并发事故:大促开售前10分钟,Redis集群CPU飙到92%,数据库连接池被打满,商品详情接口p99从120ms升到2.4s,错误率突破9%。复盘发现,问题不是“缓存不够大”,而是三类风险叠加:黑产用不存在的商品ID高频查询,造成缓存穿透;爆款Key在同一秒集中过期,造成缓存雪崩;热点Key失效后大量请求同时回源,造成击穿。更隐蔽的是,所有Key的TTL都按“基础值+固定偏移”生成,偏移只有30秒,等于把过期时间排成了队。我们此前只监控缓存命中率,没有监控“回源放大比”和“空值查询占比”,所以告警来得太晚。
治理思路
我们没有直接扩容Redis,而是按“先挡无效流量、再削回源峰值、最后做故障隔离”的顺序治理。第一层,在网关和服务本地增加参数校验与限流,非法ID直接拒绝;第二层,引入布隆过滤器拦截不存在的商品ID,并配合短TTL的空值缓存兜底;第三层,热点数据采用逻辑过期+异步重建,避免物理过期瞬间击穿;第四层,所有TTL加入随机抖动,热点Key做多副本分散;第五层,数据库前加信号量隔离和熔断降级,保证核心交易不被拖垮。原则是:缓存层可以短暂不一致,但数据库不能被打穿。
实现细节
实现上,商品详情读路径改为“本地缓存→Redis→布隆过滤器→数据库”。本地缓存用Ristretto,容量2GB,TTL 200ms,只缓存极热Key;Redis Cluster按商品ID分片。布隆过滤器放在RedisBloom,按天全量重建,并在商品新增/删除时写一条延迟双删消息,避免新商品被误判。空值缓存TTL设为15秒,值统一为特殊标记,防止黑产用随机ID反复打穿。热点Key重建用Redis SETNX抢锁,锁租期3秒,拿到锁的线程刷新缓存,拿不到锁的线程等待50ms后读旧值,最多重试2次;同时用singleflight合并同进程回源请求。逻辑过期结构为value+expireAt,读到过期数据先返回旧值,再投递异步刷新任务,保证p99不被重建阻塞。TTL抖动改为基础TTL的±20%,热点Key复制3份,副本Key加随机后缀,读取时随机选一个。数据库侧用20个信号量做隔离,超过阈值直接返回降级页;非核心字段走异步补偿。监控新增回源QPS、空值命中率、锁等待时长、热点Key集中度四个指标。
量化结果
治理上线两周后,效果可量化:商品详情接口峰值QPS 8.6万,缓存命中率从97.1%提升到99.3%;数据库回源QPS从12万降到1.8万,下降85%;p99从2.4s降到180ms;错误率从9.2%降到0.02%;Redis CPU峰值从92%降到61%。大促期间没有再出现连接池打满。成本方面,本地缓存和布隆过滤器增加约6台服务资源,但数据库没有扩容,整体月成本反而下降约8%。
经验教训
复盘有三点教训:第一,负缓存必须有,但TTL不能长,否则商品上架后会出现“假不存在”;布隆过滤器要接受误判率,并设计重建窗口。第二,锁不是万能,抢锁失败要有旧值兜底,否则会把等待变成新的雪崩。第三,降级要提前演练,不能等数据库快挂了才打开。还有一点:不要迷信“缓存命中率”,它高不代表回源压力小,热点Key的集中度过高时,1%的未命中也足以打穿数据库。治理不是一次上线,而是一套持续巡检和压测机制。
适用边界
这套方案适用于读多写少、允许短时间最终一致、存在明显热点和无效查询的高并发系统,例如商品详情、用户资料、配置中心、票务库存查询。对强一致写多读少、缓存无法容忍旧值的场景,应优先做写路径削峰和数据库分片,而不是照搬逻辑过期。若团队没有布隆过滤器运维经验,可以先做参数校验、空值缓存和TTL抖动,再逐步引入热点探测与锁重建。