TP5的Cache类不是全局单例,而是按驱动类型与配置参数组合生成唯一实例;不同Redis地址或文件路径会创建独立连接,且各驱动底层行为差异显著。

缓存实例是单例,但不同配置会生成不同实例
TP5 的 Cache 类不是全局唯一单例,而是按缓存驱动类型 + 配置参数组合生成唯一实例。调用 Cache::getInstance('redis', ['host' => '127.0.0.1']) 和 Cache::getInstance('redis', ['host' => '192.168.1.100']) 会创建两个独立的 Redis 连接实例,因为它们的配置 guid 不同。
关键逻辑在 getInstance 方法里:它把 $type . to_guid_string($options) 拼成键名存入静态数组 $_instance。这意味着你不能靠“切换配置”来复用连接——改了 host、port 或 password,就是新实例,旧连接不会自动释放。
- 开发时容易误以为改 config 就能热切换缓存后端,实际需重启或手动
Cache::clear()清掉旧实例缓存 - 多环境共用同一份代码但不同 Redis 地址时,
getInstance会缓存多个连接,长期运行可能耗尽连接数 - 文件缓存(
type => 'file')也遵循该机制,不同path配置会产生不同实例,各自管理自己的目录
所有缓存驱动都实现统一接口,但行为差异极大
TP5 抽象出 set()、get()、rm() 等方法,但底层执行逻辑完全取决于驱动。比如:
-
File驱动:序列化数据写入磁盘文件,get()时反序列化;不支持原子操作,高并发下可能读到部分写入的脏数据 -
Redis驱动:调用 phpredis 扩展的setEx()/get(),支持过期时间精确控制、CAS 操作(如incr),但依赖扩展是否启用及版本兼容性 -
Memcached驱动:使用add()和get(),默认不支持原生 TTL(老版本协议),过期时间靠客户端模拟,可能不准
所以即使代码写成 Cache::set('key', $val, 300),在 File 驱动下是「最多保留 5 分钟」,在 Redis 下是「严格 5 分钟后自动失效」——这不是 TP5 的 bug,而是底层存储能力决定的。
立即学习“PHP免费学习笔记(深入)”;
缓存键名会被自动加前缀,且路径/命名空间不可控
TP5 默认对所有缓存 key 加上配置中的 prefix(如 think_),但这个前缀拼接发生在驱动层,不是统一拦截。更关键的是:File 驱动还会把 key 做哈希处理再生成文件路径,例如 user_123 可能变成 ./runtime/cache/6a/9e/user_123.php。
这种设计带来两个现实问题:
- 无法通过外部工具(如 redis-cli 或直接 ls)直观定位某个业务 key 对应的缓存项
- 想批量清理某类缓存(比如所有用户相关)时,
File驱动只能清整个目录,Redis驱动若没开 keys 功能或没设 tag,则无法按前缀扫除 - 自定义 key 时若含特殊字符(如
/、..),File驱动会过滤,但Redis不会,导致跨驱动行为不一致
S() 函数只是语法糖,不改变底层逻辑
S('user_123', $data, 3600) 看似简洁,但它只是封装了 Cache::getInstance() + set(),内部仍走完整初始化流程。它不跳过任何校验,也不绕过驱动限制。
常见误用场景:
- 在 CLI 命令中反复调用
S()写缓存,却没意识到每次调用都可能触发新的getInstance,尤其当配置动态生成时(如根据命令参数拼 host),容易泄漏连接 - 用
S('key', null)删除缓存,但没注意文档里明确说:传null是删除,传空字符串''是写入空值——这两个语义完全不同 - 认为
S()支持标签(tag),实际上只有Cache::tag()才有该能力,S()完全不识别 tag 参数
真正影响性能和稳定性的,从来不是 API 是否好看,而是你是否清楚每个调用背后触发了几次连接、写了几个文件、有没有竞争条件——这些细节藏在驱动实现里,而不是文档首页。



















