该存null值,但需用标记值(如"__NULL__")并设1–5分钟过期,读取时严格比对标记,DB写入后立即删除缓存,确保所有路径统一解释该标记。

缓存穿透时 null 值到底该不该存?
该存,但必须带短过期时间、且和业务有效数据明确区分。不存 null,攻击者反复查不存在的 ID 就能把数据库打垮;直接存永久 null,又会导致真实数据写入后长期无法命中(比如用户刚注册,缓存里还卡着旧的 null)。
- 永远不要用
set($key, null)这种裸存方式——PHP 的 Redis 扩展或 Memcached 扩展会把null序列化成空字符串或失败,行为不一致 - 统一用标记值,比如
"__NULL__"或json_encode(['type' => 'null', 'ts' => time()]),避免和合法空字符串、空数组混淆 - 过期时间建议 1–5 分钟,具体看业务容忍度:秒杀类场景可设 60 秒,后台管理类可放宽到 300 秒
redis->setex() 和 memcached->set() 对 null 处理差异
Redis 的 setex() 能安全存字符串,所以 "__NULL__" 直接可用;Memcached 的 set() 在 PHP 7.4+ 默认禁止存 null,会报 Warning 并返回 false,不是静默失败。
- Redis 场景:直接
$redis->setex($key, 120, "__NULL__") - Memcached 场景:必须先类型转换,
$mc->set($key, "__NULL__", 120)(注意第三个参数是过期秒数,不是时间戳) - 如果用了
phpredis的set()方法带选项,别漏掉EX参数,写成$redis->set($key, "__NULL__", ['EX' => 120])
怎么在读取时识别并跳过 null 缓存?
不能靠 === null 判断返回值,因为缓存未命中时多数客户端也返回 null,和“缓存了空值”无法区分。
- 统一约定:只接受
"__NULL__"字符串为有效空标记,其他情况(null、false、空字符串)都视为缓存未命中 - 示例逻辑:
$cached = $redis->get($key); if ($cached === "__NULL__") { return null; } elseif ($cached !== false) { return json_decode($cached, true); } - 别在中间层自动过滤——比如封装的 Cache 类里直接把
"__NULL__"转成null返回,会导致上层无法区分“真没数据”和“缓存里写了空”,调试时极难定位
DB 回源后怎么安全更新 null 缓存?
这是最易出错的一环:DB 查出来是空,你往缓存写了 "__NULL__",但几毫秒后 DB 插入了真实数据,缓存却还在有效期,导致脏读。
立即学习“PHP免费学习笔记(深入)”;
- 写 DB 成功后,必须立刻
delete($key),而不是set($key, $newData)—— 避免并发时新旧值覆盖错乱 - 如果用的是延迟双删(先删缓存、再写 DB、再删缓存),第二次删动作不能省,否则
null缓存会残留 - 对高一致性要求场景(如账户余额),建议加一层布隆过滤器预判 key 是否可能存在,减少打到 DB 的
null查询量
真正麻烦的不是存不存 null,而是所有读写路径是否对这个标记值有一致解释——一个地方当成空,另一个地方当成未命中,问题就藏得特别深。



















