朕喜技术(ZhenxiJS)
2026.08.30
1 分钟阅读

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] 第二次相同请求未查询数据库 → 空值缓存生效!

经验与避坑

  1. 缓存穿透和缓存雪崩/击穿的区别:
    • 穿透:查询不存在的数据(缓存和 DB 都没有)
    • 击穿:某个热点 key 过期瞬间大量请求打向 DB
    • 雪崩:大量 key 同时过期导致 DB 压力剧增
  2. 空值缓存的 TTL 不宜过长:建议 30-60 秒,防止商品后续被添加但客户端仍然收到 404。
  3. 监控数据库查询量:当 DB 查询量突增而 Redis 命中率骤降时,优先排查缓存穿透。
  4. 生产环境建议:缓存空值 + 布隆过滤器组合使用,兼顾性能与准确性。
  5. 代码可维护性:空值标记用固定字符串(如 __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

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-08-30-1600-redis

分享这篇文章