空值缓存应存明确标记值如'null'字符串,设短随机过期时间(如300±60秒),配合布隆过滤器防穿透,并在写操作时主动清理对应空值key。

空值缓存到底该存什么值
缓存穿透本质是查不到数据还反复打穿缓存直击数据库,用空值缓存就是让「查无此 ID」这个结果也进缓存。但空值不是 null、'' 或 false ——这些在 PHP 反序列化或类型判断时容易被误判为“未命中”,导致二次穿透。
实际应存一个明确的标记值,比如 '<strong>NULL</strong>' 字符串,或序列化后的 serialize(null)。Redis 中更推荐用 SET key "<strong>NULL</strong>" EX 300(5 分钟),避免永久缓存脏空状态。
- 不要用
json_encode(null),某些旧版 Redis 扩展对 JSON 解析有兼容问题 - 不要依赖
isset()判断缓存结果,改用!empty($val) || $val === '<strong>NULL</strong>' - 如果用
Memcached,注意它的get()对 false 和未命中都返回false,必须配合getResultCode()区分
如何在 Laravel/ThinkPHP 等框架里安全写空值
主流框架的缓存封装通常对空值不友好:比如 Laravel 的 Cache::get() 返回 null 时,你无法区分是“没查到”还是“缓存里真存了 null”。必须绕过高层 API,直操作底层驱动。
以 Laravel 为例:
立即学习“PHP免费学习笔记(深入)”;
$redis = Cache::getStore()->getRedis();
$redis->setex('user:999999', 300, '__NULL__');
ThinkPHP 6 则需:
cache('user:999999', '__NULL__', ['expire' => 300]);
关键点:
- 不能用
Cache::remember()或cache()的自动回写逻辑,它们会跳过空值 - 空值缓存的过期时间必须显著短于正常数据(比如 5 分钟 vs 24 小时),否则修复数据后用户还要等很久
- 在业务逻辑中,读取后立即判断是否为
'<strong>NULL</strong>',并统一转成null向上层透出,保持接口契约
空值缓存挡不住所有穿透,还得配布隆过滤器
单靠空值缓存只能防「已知无效 ID」的重复请求,对随机构造的 ID(如 user:1234567890)仍会写一堆空值,浪费内存且增加 Redis 压力。
真实高并发场景下,必须前置一层概率型过滤:
- 初始化时把所有合法 ID 的哈希值写入布隆过滤器(可用
redisbloom模块的BF.ADD) - 请求进来先
BF.EXISTS user_bf 999999,返回 0 则直接 404,不查缓存也不查 DB - 布隆过滤器有误判率(典型 0.1%),但只可能“误判存在”,不会漏掉真实存在的 ID,所以不影响正确性
空值缓存和布隆过滤器是互补关系:前者兜底,后者减负。漏掉任一环,缓存穿透压力都会指数级上升。
空值缓存的过期策略最容易被忽略
很多人设完 EX 300 就以为万事大吉,但没考虑两个现实问题:
- 缓存雪崩风险:如果大量空值在同一时刻过期,紧接着一批相同非法请求涌进来,仍会集体打穿
- 数据修复延迟:管理员刚插入 ID=999999 的用户,但空值还在缓存里,用户得等 5 分钟才能访问
应对方式:
- 对空值使用随机过期时间,比如
300 + rand(0, 60) - 提供运营后台的「清除指定空值缓存」按钮,调用
del user:999999即可 - 在新增数据的写路径里,主动
del对应的空值 key(如果存在),不用等它自然过期
空值缓存不是设个值就完事,它是个需要和业务生命周期联动的活策略。











