缓存穿透防护需空值缓存与布隆过滤器双管齐下:布隆过滤器快速拦截绝对不存在的id,空值缓存应对可能存在的空查询,二者配合兼顾性能与准确性。

缓存穿透防护:空值缓存 + 布隆过滤器必须配合用
只加空值缓存或只用布隆过滤器,都容易翻车。空值缓存能拦住重复查不存在 ID 的请求,但会把缓存空间撑满;布隆过滤器节省空间,但有误判率——它说“不存在”,那一定不存在;它说“可能存在”,实际可能还是空。所以得双管齐下。
PHP 实现时,先查 BloomFilter::mightContain($id),返回 false 就直接返回 null;返回 true 再查缓存;缓存 miss 后查 DB;DB 返回 null 就写 cache:set('user:'.$id, null, 60),不是永久存,60 秒足够防住短时刷量。
- 布隆过滤器初始化必须覆盖全量合法 ID,不能只加载当前热数据,否则漏判会导致穿透
-
null缓存的 TTL 不能超过 5 分钟,否则新注册用户短期内无法访问 - Redis 中空值建议统一用字符串
"NULL"而非 PHPnull,避免序列化歧义
预热脚本必须脱离 Web 请求上下文执行
任何在 public/index.php 或控制器里写的 if (! $cache->has(...)) { $cache->set(...) } 都是高并发雪崩入口。发布后用户一涌而入,100 个请求同时发现 key 不存在,100 次查库,数据库当场过载。
正确做法是用 PHP CLI 脚本 + Redis 分布式锁驱动预热:
- 预热脚本通过
php artisan cache:preheat:hot-products这类命令触发,由 CI/CD 流水线自动调用 - 脚本开头立刻执行
redis->set('preheat:lock:products', '1', ['NX', 'EX' => 300]),抢不到锁就 exit,不重试 - 预热数据按「列表 → 详情」分层加载:先查
SELECT id FROM products WHERE is_hot = 1,再逐个getProductDetail($id)写缓存,避免单次查询太重
分布式环境下锁失效与缓存不一致的典型坑
很多人用 file_put_contents('lock.tmp', '1') 或数据库 SELECT ... FOR UPDATE 做锁,这在多机器部署时完全无效。文件锁只对本机进程起作用,DB 行锁在预热阶段反而成瓶颈——你本想减轻 DB 压力,结果预热脚本自己成了慢 SQL 来源。
真正可用的只有 Redis 原生命令:SET key value NX EX 300。注意三个细节:
-
NX和EX必须原子执行,不能拆成exists()+setex(),否则竞态条件依然存在 - 锁过期时间(如 300 秒)要大于单次预热最大耗时,否则锁自动释放,另一个进程可能重复加载
- 预热完成后必须显式删掉锁 key,否则下次预热被卡住——别依赖 EX 自动清理,网络分区时可能失效
预热数据粒度要按访问模式切分,不能一刀切
电商首页的「热门商品列表」和「每个商品详情」访问频次、更新频率、数据体积完全不同。混在一起预热,要么列表没刷完详情就开始写,导致缓存命中率断崖下跌;要么详情预热太多,挤占 Redis 内存,把真正热的数据踢出去。
推荐用显式命名空间隔离:
- 列表类:缓存 key 用
list:hot-products:v2,TTL 设为 1 小时,配合定时任务每 45 分钟增量补全 - 详情类:key 用
detail:product:12345,TTL 设为 24 小时,仅在商品更新事件中触发单条刷新 - 绝对禁止用通配符清缓存,比如
DEL list:hot-*,会误删正在使用的版本化 key
预热不是越全越好,而是越准越稳。漏掉几条冷门数据影响小,但一次锁失效或一次空值缓存 TTL 设置错误,可能让整个服务在早高峰崩 20 分钟。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











