缓存数据反序列化失败的根源是TP6+默认用serialize()而TP5.x用json_encode(),导致旧JSON缓存被新驱动误读为null或乱码;解决方法是在自定义驱动中重写get()/doRead(),先尝试JSON解析再fallback到unserialize,并兼容无签名文件、Redis双key及第三方非标缓存。

缓存数据反序列化失败:升级后 Cache::get() 返回 null 或乱码
ThinkPHP 6.0+ 默认改用 serialize() 替代 json_encode() 存储缓存值,老版本(如 TP5.1)写入的 JSON 格式缓存,在新驱动下无法自动识别和反序列化。现象是读取旧缓存时返回 null,或解包出字符串但内容为原始 JSON 文本(比如 "{\"name\":\"admin\"}" 而非数组)。
解决思路不是“统一格式”,而是让新驱动能“向后看”:在反序列化前先尝试 JSON 解析。
- 重写缓存驱动的
get()方法,在调用父类逻辑后,对返回值做二次判断:若结果为字符串且以{或[开头,尝试json_decode($value, true) - 不要直接修改框架源码;应在自定义驱动中继承
think\cache\driver\File(或其他对应驱动),覆盖doRead()或get() - 注意:TP6.1+ 的
TagSet缓存键结构有变,老缓存中的 tag 文件可能被忽略,需同步迁移 tag 数据或清空 tag 缓存
File 驱动缓存文件结构不兼容:升级后 runtime/cache/ 下文件读不出来
TP5.x 的 File 驱动把缓存内容直接写成纯文本(JSON 或 serialize 后的字符串),而 TP6+ 默认启用 data_serialize 配置,并在文件头写入 8 字节签名(\x00\x00\x00\x00\x00\x00\x00\x01),用于标识序列化方式。旧文件无此签名,导致 doRead() 直接跳过解析。
关键不是“改文件”,而是“改读法”——让驱动对无签名文件降级处理。
立即学习“PHP免费学习笔记(深入)”;
- 在自定义
File驱动的doRead()中,先用fread($fp, 8)读头;若不等于签名,则将文件指针重置到开头,按原始字符串方式读取并尝试unserialize()或json_decode() - 避免全局开启
data_serialize = false:这会让所有新写入也退回到 JSON,丧失对资源对象、闭包等的支持 - 路径上注意:TP6 默认缓存子目录深度为 2(
xx/yy/xxx.php),而 TP5 是扁平结构;迁移脚本需按 hash 重分布旧文件,否则命中率归零
Redis 驱动 key 前缀与序列化混用:cache:xxx 里的数据变成乱码
TP5.1 Redis 驱动默认用 setex 写入纯字符串(JSON),TP6+ 则默认用 set + serialize(),且 key 自动加 think: 前缀。结果就是:同一业务 key(如 user:123)在 Redis 里实际存了两份,一份是 cache:user:123(TP5),一份是 think:user:123(TP6),且内容编码不同。
不能靠删旧 key 解决——线上可能有混合读写,必须让新驱动能读旧 key。
- 在 Redis 驱动配置中设
'prefix' => 'cache:',与旧版一致;同时关闭'serialize' => false,强制走 JSON 流程 - 更稳妥的做法是双写过渡:升级期间,新代码同时写
think:和cache:两套 key,读取时优先读think:,未命中再查cache: - Redis 的
ttl值不会因序列化方式改变而丢失,但 PHPRedis 扩展版本低于 5.3.4 时,serialize写入的二进制数据可能被截断,务必检查扩展版本
自定义驱动中如何安全判断数据格式
别依赖文件后缀或 key 名称猜格式,真正可靠的只有内容本身。TP6 的 serialize() 输出一定以 O:、a:、s: 等字符开头,而 JSON 一定是 {、[、" 或数字开头。
一个轻量判断函数比 try-catch 更高效:
private function detectAndDecode($raw)
{
if (is_string($raw) && strlen($raw) > 2) {
switch ($raw[0]) {
case 'a': case 'O': case 's': case 'i': case 'd': case 'b':
return @unserialize($raw);
case '{': case '[':
return json_decode($raw, true);
default:
return $raw;
}
}
return $raw;
}
这个逻辑要嵌进你重写的 doRead() 或 get() 里,而不是放在业务层。每次缓存读取都走一遍,成本极低,但能兜住绝大多数混合场景。
最麻烦的其实是第三方 SDK 写入的缓存(比如微信 SDK 自己用 file_put_contents 写的),它们既没签名也不遵循框架约定,只能靠人工识别特征字段+正则匹配来救急。这种数据建议升级窗口期手动导出清洗,别指望自动兼容。



















