thinkphp3 redis序列化和反序列化
在老系统重构迁移至 ThinkPHP 6 (TP6) 的过程中,ThinkPHP 3 (TP3) 的 Redis 序列化历史遗留问题是最核心的连环坑之一。
TP3 的 S('key', $value) 缓存方法和底层 Redis 驱动,在存储非字符串(如数组、对象)时,有一套独特的序列化行为。如果不了解它的底层逻辑,新系统重构后就会出现“TP6 读不出 TP3 写入的数据(报反序列化失败)”或者“TP3 读不出 TP6 写入的数据”的现象。
下面为你深度拆解 TP3 的 Redis 序列化机制,以及如何实现新旧系统完美互通与平滑重构。
一、 TP3 的 Redis 序列化与反序列化机制
TP3 的系统内置 Redis 驱动(Think\Cache\Driver\Redis.class.php)在读写时有着非常直接的逻辑:
1. 写入(序列化)
当执行 S('name', $array) 存储一个数组或对象时,TP3 的底层逻辑如下:
如果是数组或对象:直接调用 PHP 原生的
serialize($value)转为类似a:2:{s:3:"uid";i:1;...}的二进制文本字符串。如果是标量(字符串/数字/布尔值):不进行任何处理,直接原样存入 Redis。
2. 读取(反序列化)
当执行 S('name') 获取数据时,TP3 采用“盲猜/试错法”:
php
// TP3 底层核心伪代码
$value = $this->handler->get($key);
$jsonData = json_decode($value, true);
if (!is_null($jsonData)) {
return $jsonData; // 1. 优先尝试 JSON(虽然TP3默认不以json存,但框架做了兼容)
}
// 2. 如果不是 JSON,尝试用原生的 unserialize 进行反序列化
$unserializeData = @unserialize($value);
if ($unserializeData !== false || $value === 'b:0;') {
return $unserializeData;
}
// 3. 如果反序列化也失败,说明原本存的就是普通字符串,原样返回
return $value;
请谨慎使用此类代码。
二、 为什么重构到 TP6 会冲突?(致命断层)
TP6 的缓存设计改变了规则。TP6 追求严谨性,它的 Redis 缓存驱动默认对所有数据类型(包括普通字符串)强制进行序列化加工:
在 TP6 中存入字符串
"hello",Redis 里实际存的并不是hello,而是被 PHP 序列化后的s:5:"hello";(或者是根据配置进行了 JSON / Igbinary 序列化)。冲突点:
旧读新失败:TP3 去读 TP6 写入的字符串,会因为拿到了被包裹的序列化串而解析混乱。
新读旧失败:TP6 去读 TP3 写入的普通字符串,由于它不满足 PHP 序列化的特定格式(如缺少类型前缀
s:),TP6 内部执行反序列化会直接返回false导致无法拿到数据。
三、 完美的重构兼容解决方案
为了配合你之前提到的 unsession 清除逻辑,以及让 TP3 和 TP6 共用同一个 Redis 完美互通,请根据你的业务架构采用对应的处理方案:
方案 A:让 TP6 “降级”去迁就 TP3 的数据格式(最推荐)
如果旧系统的数据极其庞大,无法做大规模数据清洗,最安全的方法是定制或扩展 TP6 的缓存驱动,使其读写行为与 TP3 保持 100% 一致。
在 TP6 中新建一个自定义的兼容驱动
app\common\cache\driver\LegacyRedis.php:
php
<?php
namespace app\common\cache\driver;
use think\cache\driver\Redis;
/**
* 专门兼容 TP3 数据的 Redis 驱动
*/
class LegacyRedis extends Redis
{
/**
* 写入数据:模拟 TP3 行为(字符串不序列化,数组对象采用原生 PHP 序列化)
*/
protected function serialize($data): string
{
if (is_scalar($data)) {
return (string)$data; // 标量直接转字符串,不包裹序列化
}
return serialize($data); // 数组、对象用标准 PHP 序列化
}
/**
* 读取数据:模拟 TP3 的试错反序列化
*/
protected function unserialize(string $data)
{
// 1. 尝试原生反序列化
$unserializeData = @unserialize($data);
if ($unserializeData !== false || $data === 'b:0;') {
return $unserializeData;
}
// 2. 尝试 JSON 解码
$jsonData = json_decode($data, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $jsonData;
}
// 3. 兜底返回原样字符串
return $data;
}
}
请谨慎使用此类代码。
随后在 TP6 的
config/cache.php中,将驱动指向你的自定义类:
php
return [
'default' => 'redis',
'stores' => [
'redis' => [
// 指向你刚刚写的自定义兼容驱动类
'type' => \app\common\cache\driver\LegacyRedis::class,
'host' => '127.0.0.1',
'port' => 6379,
'prefix' => '', // 保持和 TP3 的 C('DATA_CACHE_PREFIX') 一致!
],
],
];
请谨慎使用此类代码。
方案 B:在重构的 unsession 中手动解决“原生字符串”问题
如果在某些极其敏感的旧核心业务中(比如 Session Token 解析),你不走 TP6 的 Cache 门面,而是直接用原生 PhpRedis 扩展进行解绑,你可以利用 PHP 原生的方法手动处理:
php
// 在 TP6 的业务代码、或者是你重构的 unsession 助手函数中
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
// 读取 TP3 写入的某个用户 Session 原始串
$rawValue = $redis->get('think:unsession_token_xxxx');
if ($rawValue) {
// 手动进行 TP3 式的反序列化校验
$userData = @unserialize($rawValue);
if ($userData === false && $rawValue !== 'b:0;') {
// 说明 TP3 当时存的是个没有任何序列化的纯字符串 (如纯明文 UID 或 MD5 串)
$userData = $rawValue;
}
// 执行你的 unsession 重构清除逻辑
$redis->del('think:unsession_token_xxxx');
}
请谨慎使用此类代码。
总结避坑原则
前缀(Prefix)对齐:TP3 默认没有前缀(除非配置了
DATA_CACHE_PREFIX),而 TP6 默认会带think:这样的前缀。请确保在config/cache.php中把前缀和老系统对齐,或者在代码里补齐。退出机制:旧系统
unsession删完 Redis 键后习惯直接exit;,如果你迁移到 TP6,请尽量改为return,让 TP6 的中间件能顺畅走完整个生命周期。