必须同时用布隆过滤器前置拦截+空值缓存兜底,因thinkphp默认不缓存null导致穿透;需redisbloom支持布隆过滤器,空值统一存'__null__'并严格判断,ttl设2–5分钟,且三者共用同一redis db。

缓存穿透在 ThinkPHP 项目里不是“会不会出问题”,而是“哪天数据库扛不住了”的前置信号。直接结论:必须同时用 布隆过滤器前置拦截 + 空值缓存兜底,单靠一个方案在线上高并发场景下大概率失效。
为什么 Cache::get() 和 cache() 函数拦不住穿透
ThinkPHP 的缓存封装(包括 Cache::get()、cache())默认对 null 或空值不写入 Redis —— 这是设计使然,但恰恰成了穿透的突破口。比如你查 user:999999,数据库返回 null,框架跳过写缓存,下次请求照旧穿透。
- 所有基于
Cache::remember()的逻辑都绕不开这个限制,它内部做了if ($value === null) return $default;判断,根本不会走到set() -
cache('key', null, ['expire' => 60])在 TP6/TP8 中实际写入的是空字符串'',不是null,且部分驱动会静默丢弃 - 真正可控的只有直连 Redis 驱动:
$redis = Cache::getStore()->getRedis(); $redis->setex('user:999999', 300, '__NULL__');
布隆过滤器必须在 RedisBloom 插件支持下才实用
TP8.0 自身不带布隆过滤器能力,硬靠 PHP 实现 Bitmap(如用 SETBIT)在万级 QPS 下 CPU 占用飙升,且哈希冲突控制难。生产环境只推荐 RedisBloom 方案。
- 先确认 Redis 已加载模块:
redis-cli INFO modules | grep bloom,输出含bf version:2.4或更高 - 初始化不能放中间件或控制器里,必须在服务启动时完成:
$redis->executeRaw(['BF.RESERVE', 'bf:user', '0.01', '1000000']) - 用户新增/删除时,必须同步调用
BF.ADD;否则过滤器漏数据,等于没开——别指望定时任务补,延迟会导致窗口期穿透 - 查询判断必须用原生命令:
$exists = $redis->executeRaw(['BF.EXISTS', 'bf:user', "user:{$id}"]),TP 的bfExists()方法在 phpredis 5.3.7+ 才稳定,老版本建议统一走executeRaw
空值缓存要避开 PHP 类型陷阱
写进 Redis 的空值标记必须能被精确识别,不能依赖 empty()、isset() 或松散比较。PHP 把 ''、0、null、false 全当“空”,但业务上它们含义完全不同。
- 统一用字符串标记,例如
'__NULL__',读取后必须用严格等号判断:if ($val === '__NULL__') { return null; } - TTL 控制在 2–5 分钟足够,太长会堆积无效 key;TP8 的
cache()不支持显式写'__NULL__',必须用$redis->setex() - 不要给空值加前缀或 JSON 包装,避免序列化开销和解析失败风险;
setex('user:999999', 300, '__NULL__')是最简最稳写法 - 注意 Redis 数据库隔离:布隆过滤器、空值、正常缓存必须在同一个
selectDB,否则BF.EXISTS查不到,空值也匹配不上
最容易被忽略的一点:布隆过滤器的误判率不是越低越好。设成 0.001 要多占 3 倍内存,而 0.01(1%)在百万级用户下仅需约 1.2MB,且后续空值缓存已兜底——真正危险的从来不是那 1% 的“假存在”,而是没做任何拦截的 100% 真穿透。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











