TP6.0缓存穿透非框架问题而是业务防御缺失,需结合布隆过滤器(本地单例、预估容量、同步添加)与空值缓存(结构化value、短TTL、key一致)双重防护。

TP6.0 里缓存穿透不是框架问题,是没做防御
ThinkPHP 6.0 本身不内置布隆过滤器或空值缓存逻辑,Cache::get() 和 Cache::set() 只是 Redis 客户端的简单封装。只要业务代码里没主动拦截非法 key,比如直接写 Cache::get('user:' . $id) 然后查库,那 user:-1、user:abc 这类请求就会原样穿透——框架不会帮你判断 ID 合法性,更不会自动塞空值或查布隆。
常见错误现象包括:redis.keyspace_misses 持续飙升、数据库慢查询日志里反复出现 SELECT * FROM user WHERE id = ? 返回空结果、Redis 中突然堆积大量 user:* key 且 value 是空字符串或 "null"。
- 别指望 TP6 的缓存驱动自动识别“不存在”和“非法”——它只认 key 存不存在,不认业务语义
-
Cache::set()写空值时若没设 TTL,会永久占用内存;设了但用""或null作 value,下游可能误解析为有效数据 - 在控制器里每次 new 一个布隆实例(比如用
new BloomFilter()),等于每次请求都重建位数组,完全失效
布隆过滤器必须加载到应用进程内存,不能靠 Redis 命令查
Redis 6.2+ 虽然支持 BF.RESERVE 和 BF.EXISTS,但生产环境绝不能在每次请求里走一次 Redis 网络调用去查布隆。延迟高、连接压力大、误判率还容易失控——布隆的核心价值在于本地毫秒级判断,不是远程查表。
实操要点:
- 初始化时按业务预估:比如用户表上限 5000 万,允许 0.5% 误判率,用
bloom.NewWithEstimates(50000000, 0.005),实际内存约 30MB - 新增用户后必须同步调用
bloomFilter.add(userId);删用户无法删除,只能定期重建或配合逻辑删除标记 - 查询流程必须严格为:
bloomFilter.mightContain(id)→ false 则直接返回 404;true 才继续Cache::get(cacheKey) - 布隆实例必须作为单例注入容器或全局常驻,不能在
__invoke或 action 方法里临时 new
空值缓存的 value 和 TTL 必须带业务语义
很多人写 Cache::set('user:' . $id, '', 60) 就以为完事了,结果下游把空字符串当真实数据解析,或者 Redis 内存被无效 key 慢慢吃满。空值不是“随便填个占位符”,而是要明确表达“该 ID 在数据库中无记录”。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
推荐做法:
- value 统一用结构化占位符,比如
'{"status":"not_found","ts":' . time() . '}'或固定字符串'__NULL__' - TTL 必须短,建议 60–300 秒,避免长期占用空间;不能设为 0 或不设
- key 空间必须与正常缓存一致,比如正常是
user:12345,空值也必须是user:12345,不能写成user_null:12345或加前缀后缀 - 空值写入前需确认数据库确实返回空,不能仅凭
Cache::get()为空就写——那是缓存未命中,不是数据不存在
布隆 + 空值必须串联,不能只用其中一个
单用布隆过滤器,遇到“漏加入”的合法 key(比如新注册用户还没同步进布隆)会被误判为不存在;单用空值缓存,面对恶意构造的海量随机 key(如 user:1234567890、user:abcdefg)会迅速打爆 Redis 内存。两者缺一不可,且顺序不能错。
标准链路是:
- 请求进来 → 先查布隆 → 不存在则 404(事前拦截)
- 存在 → 查
Cache::get()→ 命中则返回 - 未命中 → 查 DB → 有结果则写缓存并返回;无结果则写空值缓存(事后兜底)
- 写空值缓存前,必须确保布隆已判定“可能存在”,否则就是把非法 key 也塞进缓存
最容易被忽略的是布隆和空值 key 的一致性:布隆校验的 key 格式(如 12345)、缓存 key 格式(如 user:12345)、空值写入的 key 格式,三者必须严格对齐,差一个冒号或类型转换(int vs string)就会让整套防御失效。

















