缓存穿透是指查询确定不存在的数据导致请求持续击穿缓存直打数据库,与普通未命中本质不同;需用字符串占位符缓存空结果、布隆过滤器前置校验或接口层参数校验来防范。

缓存穿透是什么,为什么它不是“缓存没命中的正常现象”
缓存穿透特指查询一个**确定不存在的数据**(比如无效的用户ID、被删干净的订单号),导致每次请求都击穿缓存直打数据库。它和普通缓存未命中本质不同:后者是临时性缺失,前者是持续性、可预测的无效压力。
典型表现:Redis里查不到,DB 里也查不到,但请求量不小——比如爬虫扫 /user?id=999999999 这类非法ID,或恶意构造不存在的短链接。
关键区别在于:普通未命中后写入缓存(哪怕空值)能缓解;穿透则必须防止空值被反复穿透,否则 DB 会被拖垮。
用空对象(Null Object)兜底,但别直接存 None
最常用做法是把「确认不存在」的结果缓存起来,避免重复查 DB。但直接存 Python 的 None 有坑:很多序列化库(如 json、pickle)对 None 处理不一致,Redis 客户端也可能返回 None 表示连接失败,和业务上的「空结果」混淆。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 统一用字符串占位,比如
"__NULL__"或json.dumps({"_null": true}) - 设置较短的过期时间(如 2–5 分钟),避免长期占用内存又失去时效性
- 在缓存读取逻辑里显式判断该占位符,而不是依赖
is None - 如果用
redis-py,注意get()返回None可能是 key 不存在,也可能是网络异常,需结合connection_pool状态判断
布隆过滤器(Bloom Filter)前置校验,但别在 Python 进程里硬实现
布隆过滤器适合在请求进 DB 前快速拦截「肯定不存在」的 key。但它在 Python 中容易踩两个坑:一是纯 Python 实现(如 bloom-filter 包)性能差、内存占用高;二是误判率没调好,把真实存在的 key 当成不存在给拒了。
更稳妥的做法:
- 用 Redis 自带的
BF.RESERVE+BF.ADD+BF.EXISTS(需 RedisBloom 模块,6.0+ 可加载) - 初始化时批量导入已知有效 key(比如所有合法用户 ID),运行时只增不删
- 允许少量误判(
error_rate=0.01是合理起点),但绝不允许漏判——所以布隆返回False时仍要走缓存+DB 链路 - 别用
pybloom_live在应用进程里维护大过滤器,GC 和内存碎片会让服务抖动
接口层加参数校验,比缓存层补救更省事
很多缓存穿透其实源于上游没约束。比如 ID 是 8 位数字,却收到字母或超长字符串;或者短链哈希是 6 位 base62,却来了 12 位乱码。这类请求根本没必要进缓存逻辑。
建议在 Web 框架路由或中间件里做轻量校验:
- 用正则预筛
user_id:比如r"^\d{1,10}$",不匹配直接 400 - 对短链类字段,先检查长度和字符集,再查缓存
- 不要在
get_user_from_cache()里做这些——校验越靠前,越早释放资源 - 注意:Django 的
URLConf或 FastAPI 的Path参数类型提示(如int)能自动拦截非数字,但不会防住超大整数溢出,得额外限制范围
真正难处理的是那些「格式合法但语义无效」的穿透,比如存在过的用户被彻底注销,ID 依然符合规则。这种才需要空对象或布隆过滤器配合——但大多数线上穿透,其实是连第一道门都没守好。










