thinkphp缓存穿透防护需三环节协同:布隆过滤器前置拦截非法key、空值标记直连redis原子写入、三者共用同一redis db,缺一不可。

ThinkPHP部署后缓存穿透不是配置开关就能关掉的问题,而是必须在数据写入、缓存读取、请求拦截三个环节同时加固。核心就两条:布隆过滤器前置拦截非法key + 空值标记严格写入并识别,缺一不可。
必须用布隆过滤器做第一道防线
部署后若没启用布隆过滤器,所有不存在的 ID(比如 user:999999、goods:1234567890)都会绕过缓存直打数据库。TP8.0 本身不带该能力,需确认 Redis 已加载 RedisBloom 模块:
- 执行 redis-cli INFO modules | grep bloom,看到 bf version:2.4 或更高才可用
- 服务启动时初始化:$redis->executeRaw(['BF.RESERVE', 'bf:user', '0.01', '1000000'])(误判率1%,容量100万)
- ID 新增/删除时同步调用 BF.ADD bf:user "user:123",漏同步等于白配
- 查询前先判断:$exists = $redis->executeRaw(['BF.EXISTS', 'bf:user', "user:{$id}"]),返回 false 就直接返回 404,不查缓存也不查库
空值缓存不能依赖 cache() 函数
部署后如果只用 cache('user:9999', null, [...]),Redis 里根本不会写入任何内容——ThinkPHP 对 null、false、空数组等 falsy 值默认跳过存储,这就是穿透持续发生的原因。
- 必须绕过封装,直连 Redis 驱动:$redis = Cache::getStore()->getRedis()
- 写空值用原子命令:$redis->setex('user:9999', 300, '__NULL__')(TTL 设 2–5 分钟)
- 绝对不要分两步:$redis->set() + $redis->expire(),非原子操作易留永久脏 key
- 读取后严格判断:if ($val === '__NULL__') { return null; },禁用 empty()、isset() 等松散判断
三者必须共用同一个 Redis DB
布隆过滤器、空值缓存、正常业务缓存若不在同一个 Redis 数据库(db),会出现逻辑断层:BF 判断存在但缓存为空,或空值已删而 BF 还在放行。
- 在 config/cache.php 的 redis 配置中显式指定:'select' => 1(避免用默认 db 0)
- 所有 BF.RESERVE、setex、Cache::get() 都走这个库
- 上线前用 redis-cli -n 1 keys "*" 检查三类 key 是否共存
上线后必须验证和监控
部署不是终点,而是防护生效的起点。不验证等于没做。
- 用压测工具模拟恶意请求:user:999999999,观察 DB QPS 是否归零、Redis 中是否无对应空值 key
- 在日志中记录 BF.EXISTS 返回 true 但 DB 查无结果的情况,用于反向校准布隆参数
- 监控 Redis 内存中 __NULL__ 类 key 的数量增长趋势,异常飙升说明有新类型穿透未被 BF 拦住
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











