缓存穿透基础
一个真实的故事
那是一个普通的周二下午。
突然,监控系统发出警报:
⚠️ 警告:外部 API 调用失败率飙升到 80%
⚠️ 警告:API 响应时间超过 5 秒
⚠️ 警告:错误率上升到 30%我冲回电脑前,查看日志,发现了异常:
2025-03-15 14:23:15 - GET /weather?city=invalid_city_1 → 404
2025-03-15 14:23:15 - GET /weather?city=invalid_city_2 → 404
2025-03-15 14:23:16 - GET /weather?city=fake_city_999 → 404
...有人在恶意攻击我的 API!
什么是缓存穿透?
缓存穿透攻击流程
恶意攻击者
构造大量不存在 key
缓存层 (Redis)
invalid_001 不存在
invalid_002 不存在
invalid_003 不存在
外部 API
GET /api/weather?city='invalid_001' 200ms
GET /api/weather?city='invalid_002' 195ms
GET /api/weather?city='invalid_003' 210ms
限流状态:
已触发
外部 API 触发限流!
缓存穿透是指:
- 查询一个不存在的数据
- 缓存层没有(因为数据不存在)
- 请求直接穿透到外部 API
- 如果大量这样的请求同时到来,外部 API 可能被压垮
关键特征:
- 查询的数据在缓存和外部 API 中都不存在
- 每个请求都会访问外部 API
- 恶意攻击者可以利用这个漏洞
攻击原理
正常请求 vs 穿透攻击
正常请求流程:
用户请求 → 查缓存(命中)→ 直接返回
↓
查缓存(未命中)→ 调用外部 API → 写入缓存 → 返回穿透攻击流程:
恶意请求 1 → 查缓存(无)→ 调用外部 API(404)→ 返回空
恶意请求 2 → 查缓存(无)→ 调用外部 API(404)→ 返回空
恶意请求 3 → 查缓存(无)→ 调用外部 API(404)→ 返回空
...问题所在:
- 因为数据不存在,所以无法写入缓存
- 每个请求都直接调用外部 API
- 攻击者可以构造大量不存在的 key
- 外部 API 有调用频率限制,容易被触发
真实攻击案例
# 攻击者构造的恶意请求
GET /api/weather?city=invalid_001
GET /api/weather?city=invalid_002
GET /api/weather?city=invalid_003
...
GET /api/weather?city=invalid_99999
# 每秒 1000 个这样的请求
# 每个请求都调用外部 API
# 很快触发了 API 限流当前技术架构
每个请求都直接调用外部 API,响应慢
客户端
用户请求 200 个开发者
应用服务
应用服务器 处理请求
缓存层 / 外部服务
外部天气 API 响应时间 2 秒
引入 Redis 缓存,大幅降低响应时间
客户端
用户请求 高并发访问
应用服务
应用服务器 处理请求
缓存层 / 外部服务
Redis 缓存 1 小时过期
外部天气 API 响应时间 2 秒
增加空值缓存,防止缓存穿透
客户端
用户请求 包含恶意请求
应用服务
应用服务器 参数校验
缓存层 / 外部服务
Redis 缓存 包含空值缓存
外部天气 API 有调用限制
使用布隆过滤器提前过滤无效请求
客户端
用户请求 包含恶意请求
应用服务
应用服务器 布隆过滤器校验
缓存层 / 外部服务
Redis 缓存 包含空值缓存
外部天气 API 有调用限制
使用互斥锁防止缓存击穿
客户端
用户请求 高并发访问
应用服务
应用服务器 互斥锁控制
缓存层 / 外部服务
Redis 缓存 热点 key 防护
外部天气 API 有调用限制
熔断器 + 降级策略应对缓存雪崩
客户端
用户请求 高并发访问
应用服务
应用服务器 熔断器 + 降级
缓存层 / 外部服务
Redis Sentinel 主从高可用
外部天气 API 有调用限制
多地域部署的高可用 API 平台
客户端
用户请求 10 万用户
应用服务
应用服务器集群 26 台,3 地域
缓存层 / 外部服务
Redis 集群 6 主 6 从
MySQL 主从 1 主 5 从
消息队列 RabbitMQ 3 台
练习
练习 1
缓存穿透的根本原因是什么?它与缓存击穿、缓存雪崩有什么区别?
参考答案 (2 个标签)
缓存穿透 基础概念
答案:
缓存穿透的根本原因:查询的数据在缓存和外部 API 中都不存在。
具体分析:
- 请求的 key 在缓存中不存在
- 请求的 key 在外部 API 中也不存在(返回 404)
- 因此无法将结果写入缓存
- 每次请求都会调用外部 API
恶意利用: 攻击者可以构造大量不存在的 key,使所有请求都调用外部 API,导致 API 限流或服务不可用。
与缓存击穿、缓存雪崩的区别:
| 问题 | 根本原因 | 数据是否存在 | 典型场景 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 不存在 | 恶意攻击、参数错误 |
| 缓存击穿 | 热点数据过期 | 存在 | 热点新闻、明星数据 |
| 缓存雪崩 | 大量数据同时过期 | 存在 | 批量导入、定时任务 |
练习 2
假设你的 API 遭受了缓存穿透攻击,每秒有 1000 个请求查询不存在的数据,外部 API 的限流是每秒 100 次。请分析:
- 如果不防护,会发生什么?
- 如果使用缓存空对象方案(空值缓存 60 秒),能减少多少 API 调用?
- 如果使用布隆过滤器(误判率 0.1%),能减少多少 API 调用?
参考答案 (2 个标签)
缓存穿透 效果分析
答案:
1. 不防护的情况:
- 每秒 1000 个请求都调用外部 API
- API 限流是每秒 100 次
- 结果:900 个请求失败(90% 失败率)
- 系统表现:大量错误,用户体验极差
2. 使用缓存空对象方案:
- 第 1 秒:1000 个请求都调用 API(100 成功 + 900 被限流)
- 第 2-60 秒:所有请求从缓存返回,0 次 API 调用
- 第 61 秒:如果攻击持续,重复上述循环
API 调用减少:
- 无防护:1000 请求/秒 × 60 秒 = 60,000 次
- 有防护:1000 请求(第 1 秒)+ 0(第 2-60 秒)= 1,000 次
- 减少:59,000 次(约 98.3%)
3. 使用布隆过滤器:
- 误判率 0.1%,意味着 0.1% 的不存在数据会被误判为可能存在
- 每秒 1000 个请求,布隆过滤器拦截 999 个
- 只有 1 个请求(0.1%)会穿透到 API
API 调用减少:
- 无防护:1000 请求/秒 × 60 秒 = 60,000 次
- 有布隆过滤器:1 请求/秒 × 60 秒 = 60 次
- 减少:59,940 次(约 99.9%)
结论:
- 缓存空对象:减少约 98% 的 API 调用,简单有效
- 布隆过滤器:减少约 99.9% 的 API 调用,防护更强
练习 3
在什么情况下应该选择缓存空对象方案?在什么情况下应该选择布隆过滤器方案?
参考答案 (2 个标签)
缓存穿透 方案选择
答案:
选择缓存空对象方案的情况:
✅ 非法 key 数量较少(< 1 万) ✅ 希望实现简单,快速上线 ✅ 需要 100% 准确,不能有误判 ✅ 内存资源充足 ✅ 数据一致性要求高(不能有误判)
典型场景:
- 小型 API 服务
- 内部系统
- 参数格式固定的场景
- 可以接受短暂的数据不一致
选择布隆过滤器方案的情况:
✅ 非法 key 数量巨大(> 10 万) ✅ 内存资源紧张 ✅ 可以接受小幅误判(0.1% 左右) ✅ 有明确的合法数据集合 ✅ 需要极高的拦截率
典型场景:
- 大型互联网应用
- 面向公网的 API
- 用户 ID、商品 ID 等海量 key
- 对性能要求极高的场景
组合使用的情况:
✅ 防护要求极高的场景 ✅ 不能容忍单点失效 ✅ 资源充足,追求极致性能
组合架构:
请求 → 布隆过滤器(第一层)→ 缓存空对象(第二层)→ 外部 API
拦截 99.9% 拦截剩余 0.09%