前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 27|回复: 0

[开发应用] Redis 缓存穿透与雪崩防护实战:从布隆过滤器到限流降级

[复制链接]

[开发应用] Redis 缓存穿透与雪崩防护实战:从布隆过滤器到限流降级

[复制链接]
dbaai

主题

0

回帖

334

积分

DBAAI

积分
334
昨天 07:54 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
一、具体的问题


某天上午商品详情页接口 P99 从 80ms 涨到 2.3s,MySQL CPU 打到 95%,慢查询日志里全是同一条 SQL:SELECT * FROM item WHERE item_id = ?。诡异的是这条查询走主键,单次执行不到 1ms。真相是三类缓存失效问题叠加在一起:爬虫在批量枚举不存在的 item_id(缓存穿透);秒杀热点 key 集中在整点过期,过期瞬间上千并发同时打到 DB(缓存击穿);前一天晚上批量导入任务给十几万个 key 设了完全相同的 TTL 86400 秒,次日凌晨集中失效(缓存雪崩)。三种失效混在一起,缓存形同虚设,DB 直接被打穿。这篇文章记录当时的完整排查与治理过程。

二、核心原理


先把三个概念掰清楚,它们经常被混为一谈,但根因和药方完全不同:


  • 穿透:查询的数据在缓存和 DB 里都不存在。每次请求都绕过缓存直达 DB。根因是"缓存不会缓存'不存在'这个事实"。
  • 击穿:某个热点 key 过期的一瞬间,成百上千个并发同时未命中,一起涌向 DB。根因是过期瞬间的并发没有互斥。
  • 雪崩:大量 key 在同一时间集中过期,或者 Redis 实例本身挂掉。根因是过期时间或可用性没有做隔离。


三个容易误判的点:


  • 布隆过滤器判"不存在"是一定准的,判"存在"却可能是误判——所以它只能拦截穿透流量的大头,误判漏网的部分还得靠空值缓存兜底。另外标准布隆过滤器不支持删除,硬删会把别的 key 也误判成不存在,要么用计数布隆,要么定期重建。
  • 击穿的"互斥锁"不是加了分布式锁就完事——拿到锁的线程负责回填缓存,其余线程应该短暂等待后重读缓存拿到数据,而不是直接抛异常把错误放大给用户。
  • 雪崩最常见的根因其实不是 TTL 撞车,而是 Redis 单实例没有高可用。TTL 随机抖动只解决"集中过期",解决不了实例挂掉;应用层限流降级才是最后一道防线。


三、实例参考(动手步骤)


步骤 0:确认现状基线
  1. redis-cli info stats | grep -E 'keyspace_hits|keyspace_misses'
  2. # 命中率 = hits / (hits + misses)
复制代码
  1. SHOW GLOBAL STATUS LIKE 'Threads_running';
复制代码

当时命中率只有 71%(健康值应大于 95%),misses 的突增曲线与 MySQL CPU 尖峰完全同步,先确认"DB 被 miss 流量打穿"这个大方向。

步骤 1:定位 miss 流量来源
  1. redis-cli slowlog get 20
复制代码

在应用侧临时加日志抽样记录未命中的 key,再回 DB 验证:
  1. SELECT COUNT(*) FROM item WHERE item_id IN (/* 抽样的 100 个未命中 key */);
复制代码

结果 40% 的 miss 来自根本不存在的 item_id(爬虫枚举 id 空间),这就是穿透源头。

步骤 2:布隆过滤器前置拦截

安装 RedisBloom 模块后:
  1. BF.RESERVE item_bf 0.001 150000000
复制代码

误判率 0.1%,1.5 亿容量约占 180MB。上线前先用离线任务把全量 item_id 灌进去(BF.ADD item_bf <id> 循环或 pipeline 批量)。读路径改造为:先 BF.EXISTS item_bf <id>,返回 0 直接返回"商品不存在",不再碰缓存和 DB。

注意:标准布隆过滤器不支持删除,商品下架不删过滤器元素,漏网的由下一步兜底。

步骤 3:空值缓存兜底
  1. val = r.get(key)
  2. if val is None:
  3.     data = db.query(item_id)
  4.     if data is None:
  5.         r.set(key, "", ex=60)      # 空值,短 TTL
  6.         return None
  7.     r.set(key, json.dumps(data), ex=base_ttl)
复制代码

布隆误判的漏网流量、过滤器上线前的存量穿透都由它兜住。TTL 60 秒足够:太长会导致商品上架后一段时间内"查无此货"。

步骤 4:热点 key 互斥回填(防击穿)
  1. def get_item(key, item_id, retry=3):
  2.     val = r.get(key)
  3.     if val is not None:
  4.         return json.loads(val)
  5.     if retry <= 0:
  6.         return db.query(item_id)   # 兜底直查,宁可压 DB 也不能死循环
  7.     if r.set("lock:" + key, 1, nx=True, ex=5):
  8.         data = db.query(item_id)
  9.         r.set(key, json.dumps(data), ex=86400)
  10.         r.delete("lock:" + key)
  11.         return data
  12.     time.sleep(0.05)
  13.     return get_item(key, item_id, retry - 1)
复制代码

另一种做法是逻辑过期:value 里带 expire_at 字段,发现逻辑过期后不删 key,由异步线程刷新,读请求永远拿得到旧数据,彻底消除"过期空窗"。

步骤 5:TTL 随机抖动(防集中过期)
  1. ttl = 86400 + random.randint(0, 3600)
复制代码

批量导入任务里原本统一的 EXPIRE key 86400 全部改成上述写法,把过期时间摊开到一个小时的区间里,削平失效尖峰。

步骤 6:限流与降级最后防线


  • 应用层对详情接口加单机限流(信号量 500 QPS),超限直接返回上一次快照的静态兜底页。
  • Redis 整体不可用时,读请求降级到本地二级缓存(Guava Cache,容量 10 万条、TTL 5 分钟)——数据可能旧五分钟,但比把 DB 打穿强得多。


步骤 7:验证


  • 用 wrk 回放 10 万个不存在 id 的穿透流量,确认 DB QPS 增量接近 0。
  • 观察命中率回升到 97% 以上、Threads_running 回落到个位数,才算治理闭环。


四、实操检查清单



  • 缓存命中率监控:hit ratio < 95% 告警,突降必须当天定位。
  • 布隆过滤器:容量按预估 key 数 1.5 倍设置,误判率 ≤ 0.1%;每月核对 BF.INFO 的 numItems 与业务表行数偏差。
  • 空值缓存 TTL ≤ 5 分钟;写路径创建数据后必须删除对应空值 key。
  • 所有写缓存路径的 TTL 必须加随机抖动(基础值 ±10% 以上)。
  • 热点 key 用 redis-cli --hotkeys 每日巡检;秒杀类 key 提前预热并采用逻辑过期。
  • 互斥锁必须带 ex 超时,重试必须有次数上限和 DB 直查兜底。
  • 应用层限流阈值必须压测演练过,兜底页不能是"纸面方案"。
  • Redis 本身要哨兵或集群高可用,每月做一次故障切换演练。


几个容易踩的坑



  • 布隆过滤器没配空值缓存:误判的流量照样打到 DB,过滤器等于白装——两者必须成对上线。
  • 空值缓存 TTL 设几小时:商品上架后长时间"查无此货",客诉比慢查询更难解释。
  • 互斥锁不设过期时间:持锁线程一旦崩溃,这个 key 永远没人能回填,等于给缓存埋了颗雷。
  • 把布隆的"可能存在"当"一定存在":它只能用于读路径拦截,写路径用它判断会漏数据。
  • 限流阈值拍脑袋:没压测过的降级链路,真触发时兜底页自己先 5xx,比不降级还难看。


治理前后对比


指标治理前治理后
缓存命中率71%98.6%
MySQL CPU 峰值95%12%
详情接口 P992.3s82ms
穿透流量到 DB约 4200 QPS< 5 QPS
热点过期瞬间 DB 尖峰3800 QPS单次回填(互斥锁)
Redis 故障演练无每月 1 次,降级链路验证可用


一句话总结:穿透靠"前置拦截 + 空值兜底",击穿靠"互斥回填 + 逻辑过期",雪崩靠"TTL 抖动 + 限流降级"——三道防线各管各的,缺一道,DB 就替缓存还债。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-17 06:09 , Processed in 0.018873 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表