不能用 cache() 写空值,因其对 null 等 falsy 值静默过滤,跳过存储逻辑,导致 Redis 无写入、穿透依旧;必须用原子命令 setex 写入显式标记字符串(如 '__NULL__')并严格判断 ===。

直接存 null 或空字符串会失效,必须用显式标记字符串(如 '__NULL__')配合原子写入命令,否则缓存层无法区分“未命中”和“已缓存空值”。
为什么不能用 cache() 写空值
ThinkPHP 的 cache() 函数对 null、false、空数组等 falsy 值做了静默过滤——它根本不会把它们写进 Redis。你调用 cache('user:9999', null, ['expire' => 300]),Redis 里什么都不会出现,下次请求照样穿透。
- 源码层面:
cache()内部会先判断!$value,为真就直接返回,跳过存储逻辑 - 后果:你以为加了空值缓存,实际等于没加,DB 仍在裸奔
- 正确做法:绕过
cache(),直连底层驱动或用Cache::store()->getAdapter()->set()
怎么安全写入空值标记
必须用原子命令 SETEX(即 setex),且值为不可伪造的字符串,比如 '__NULL__'。不能用 json_encode(null),某些旧版 Redis 扩展解析 JSON 时会出错;也不能用 serialize(null),反序列化后仍是 null,又回到误判原点。
- Laravel 不适用,但 TP6/TP8 可用:
cache('user:9999', '__NULL__', ['expire' => 300])—— 这个cache()是函数重载版,支持字符串值写入 - 更稳妥(推荐):
$redis = Cache::getStore()->getRedis(); $redis->setex('user:9999', 300, '__NULL__'); - 绝对避免:
$redis->set('user:9999', '__NULL__'); $redis->expire('user:9999', 300);—— 非原子,崩溃或中断会导致永久脏 key
读取后怎么识别并透出空值
从缓存取出来的值,必须显式比对是否等于你的标记字符串。不能用 empty()、isset() 或 is_null(),因为这些函数对 '__NULL__' 全部返回 false,但对真实业务返回的空字符串 ''、数字 0、布尔 false 也返回 true,完全无法区分。
立即学习“PHP免费学习笔记(深入)”;
- 正确判断:
if ($val === '__NULL__') { return null; } - 错误写法:
if (empty($val)) { ... }—— 会把合法的user.name = ''也当成穿透处理 - 注意:返回给上层的必须是 PHP
null,保持接口契约;标记字符串只在缓存层内部流转
TTL 设多少才不翻车
空值过期时间不是拍脑袋定的。设太短(如 10 秒),刚过期又被新请求打穿;设太长(如 2 小时),用户刚注册完,查自己 ID 却返回“不存在”,体验崩坏。
- 用户类数据(ID 稳定、新增少):
300秒(5 分钟)较稳妥 - 商品/订单类(频繁上下架):
60秒,再配合后台异步刷新机制 - 千万别全局硬编码一个值——不同模块的数据变更节奏差异很大,混用 TTL 会导致一部分模块永远“看不见”新数据
空值缓存只是保底手段,它挡不住随机构造的非法 ID(比如 user:1234567890)。真正要压住穿透流量,布隆过滤器必须前置部署,且写 DB 时同步更新;否则空值越写越多,Redis 内存迟早被撑爆。



















