Redis 缓存穿透(Cache Penetration)解决方案:缓存空值实战
问题背景
在电商系统中,用户通过 API 查询商品详情,正常流程为「先查 Redis → 未命中查数据库 → 回填缓存」。但当攻击者或爬虫大量请求不存在的商品 ID(如 /product/999999999),这些 key 在 Redis 中永远不存在,每次请求都会穿透缓存直达数据库,导致数据库查询量暴涨到正常值的 100 倍以上,甚至引发宕机。
这就是经典的「缓存穿透(Cache Penetration)」问题:查询的数据在缓存和数据库都不存在,每次请求都绕过缓存直达数据库。
复现步骤
# 1. 启动环境
cd scenario/2026-08-30-1600
docker compose up -d --build
# 2. 执行复现脚本(500 个恶意请求)
docker compose exec tester python3 /workspace/scripts/reproduce.py --requests 500
典型报错/现象:
[FAIL] 缓存穿透复现成功!
发送了 500 个恶意请求,数据库被查询了 500 次(几乎 1:1 穿透)。
关键指标:
- 发送 500 个恶意请求 → 数据库查询量增加 500 次(1:1 穿透)
- Redis key 数量不增长(因为不存在的数据未回填缓存)
根本原因
app.py 中的缓存回填逻辑有缺陷:
# 缺陷代码:只有商品存在时才回填缓存
if product:
r.setex(cache_key, CACHE_TTL, json.dumps(product))
else:
# 商品不存在:直接返回 404,但不回填缓存 ← 问题所在
return jsonify({"error": "Product not found"}), 404
根因:只对存在的 key 回填缓存,对不存在的 key 不回填。导致不存在的 key 每次都穿透到数据库。
解决办法
方案一(本实验采用):缓存空值
当商品不存在时,也向 Redis 写入特殊标记 __NULL__,设置较短 TTL(如 60 秒)。后续相同 key 的请求直接命中缓存返回 404,不再穿透数据库。
NULL_CACHE_TTL = 60 # 空值缓存 60 秒
# 读取缓存时检测空值标记
cached = r.get(cache_key)
if cached == "__NULL__":
return jsonify({"error": "Product not found"}), 404
# 查询数据库后,无论结果如何都回填
if product:
r.setex(cache_key, CACHE_TTL, json.dumps(product))
else:
r.setex(cache_key, NULL_CACHE_TTL, "__NULL__") # 缓存空值
优点:实现简单、不需要额外组件、对已有代码改动小。 缺点:缓存了不存在的数据,可能有少量内存浪费(可通过短 TTL 缓解)。
方案二:布隆过滤器(Bloom Filter)
在 Redis 前加一层布隆过滤器,判断 key 是否可能存在。不存在的 key 直接拦截,不查缓存也不查数据库。
优点:不占 Redis 内存,查询效率极高。 缺点:需要额外引入组件(如 RedisBloom),且存在误判率(false positive)。
方案三:接口限流 + 参数校验
在 API 入口校验参数格式/范围,对高频访问做限流。
优点:实现简单,无需改缓存逻辑。 缺点:无法完全阻止大量合法格式但不存在 ID 的穿透请求。
验证
修复后重新运行复现脚本:
docker compose down && docker compose up -d
docker compose exec tester python3 /workspace/scripts/reproduce.py --requests 500 --hot-requests 50
修复后输出:
[PASS] 未观察到明显缓存穿透(数据库增量 403 < 请求量 * 0.9)。
二次验证(相同不存在 ID 查询两次):
第一次查询 ID=8888 -> HTTP 404,DB 增量: +1
第二次查询 ID=8888 -> HTTP 404,DB 增量: 0
[PASS] 第二次相同请求未查询数据库 → 空值缓存生效!
经验与避坑
- 缓存穿透和缓存雪崩/击穿的区别:
- 穿透:查询不存在的数据(缓存和 DB 都没有)
- 击穿:某个热点 key 过期瞬间大量请求打向 DB
- 雪崩:大量 key 同时过期导致 DB 压力剧增
- 空值缓存的 TTL 不宜过长:建议 30-60 秒,防止商品后续被添加但客户端仍然收到 404。
- 监控数据库查询量:当 DB 查询量突增而 Redis 命中率骤降时,优先排查缓存穿透。
- 生产环境建议:缓存空值 + 布隆过滤器组合使用,兼顾性能与准确性。
- 代码可维护性:空值标记用固定字符串(如
__NULL__),确保不会与真实数据混淆。
技术栈
- Redis 7.x
- Python 3.11 + Flask + redis-py
- Docker / Docker Compose
- 模拟数据库(内存 dict)
可复现命令
git checkout 2026-08-30-1600
cd scenario/2026-08-30-1600
docker compose up -d --build
docker compose exec tester python3 /workspace/scripts/reproduce.py
