Redis 缓存穿透:生产环境踩坑与四种防御策略
去年大促期间,我们负责的一个活动接口被刷崩了。不是流量太大,是有人用不存在的商品 ID 疯狂请求,Redis 没命中,请求全打到数据库,连接池直接打满。
这就是典型的缓存穿透。事后复盘,我把几种处理方案整理了一下,顺便说说实际落地时的坑。
空值缓存:最简单的方案,也是最容易出问题的
查询结果为空时,把空值写进 Redis,设一个较短过期时间。实现快,能挡住大部分恶意请求。
但坑很明显:攻击者用随机 key 来刷,缓存里会堆积大量无用空值,内存持续膨胀。我们第一次就这么处理的,结果 Redis 内存告警,半夜起来清理。
后来给空值加了统一前缀,过期时间拆成随机区间,避免同时失效造成缓存雪崩。
布隆过滤器:升级后的方案,维护成本是隐形成本
把所有合法 key 提前加载进过滤器,请求进来先过一遍,不存在的直接拦截。
问题是布隆过滤器不支持删除,业务频繁变更 key 集合时维护成本高。我们的做法是定时重建,用两个实例交替切换,保证服务不中断。
误判率也要调好,太高误杀正常请求,太低过滤器体积膨胀,需要权衡。
接口限流:从攻击源头控制
对单个 IP 或用户做请求频率限制。穿透往往是高频请求触发的,限流能把压力降下来。
我们配合网关层做了令牌桶限流,业务层加了异常 key 统计窗口。某个 key 短时间内被请求超过阈值,直接拉黑一段时间。
坑在于容易误伤正常用户,比如公司内网出口 IP 是同一个。后来拆成多维度:IP + 用户 ID + 设备指纹,降低误杀率。
异步回源加互斥锁:解决并发穿透的核心
数据库压力大的根本原因是多个请求同时去查库。加了互斥锁,同一个 key 只允许一个线程回源,其他请求等待或返回降级数据。
用分布式锁实现,拿到锁的线程去查库,没拿到的要么等要么走兜底逻辑。锁的过期时间要设好,太短回源没完成锁就释放了,太长异常情况下其他请求一直阻塞。
这个方案对代码侵入性大,我们在基础组件层统一封装,业务方无感知。
生产环境的最终组合
单一方案都有短板,我们最后是组合使用:布隆过滤器做第一层拦截,空值缓存兜底,限流控制频率,互斥锁保护数据库。
踩过的几个坑
空值缓存过期时间别设太长,否则内存爆炸。布隆过滤器重建时记得预热,直接切换会有瞬间大量误判。互斥锁异常释放后要有兜底机制避免死锁。
一点想法
缓存穿透看起来是小问题,但攻击成本低、破坏力大。方案选型要看业务特点:读多写少用布隆过滤器,变化频繁用空值缓存加限流,高并发场景必须加互斥锁。
你们生产环境是怎么处理的?有没有更优雅的实现?评论区聊聊。
