
用户信息存储:把多字段数据“打包”存进数据库
做用户中心的时候,你肯定遇到过这样的问题:用户表要存昵称、性别这些基础字段,还要存“偏好设置”——比如喜欢的颜色、通知方式、页面布局,这些是数组或对象,总不能给每个偏好都建一个字段吧?去年我帮朋友做博客用户中心时,他一开始把“偏好设置”拆成了prefer_color
、prefer_font
、prefer_notify
三个字段,每次更新都要改三行SQL,查的时候还要拼三个字段,后来我让他用序列化把这些偏好“打包”成一个字符串,存到一个preferences
字段里,瞬间省了一半工作量。
具体怎么操作?比如用户的偏好是一个数组:
$userPreferences = [
'color' => 'dark',
'font' => '微软雅黑',
'notify' => ['email', 'wechat']
];
用serialize()
函数把数组转成可存储的字符串:
$serializedPrefs = serialize($userPreferences);
// 结果长这样:a:3:{s:5:"color";s:4:"dark";s:4:"font";s:12:"微软雅黑";s:6:"notify";a:2:{i:0;s:5:"email";i:1;s:6:"wechat";}}
然后把这个字符串存到数据库的preferences
字段里——注意字段类型要选TEXT
或LONGTEXT
,避免字符串太长被截断(我朋友一开始用VARCHAR(255)
,结果偏好数组稍大就被截断,反序列化时直接报错)。
等要读取用户偏好时,用unserialize()
把字符串“拆包”回数组:
$user = DB::table('users')->find($userId);
$preferences = unserialize($user->preferences);
// 现在$preferences就是原来的数组,直接用就行
echo $preferences['color']; // 输出dark
是不是比拆成多个字段方便多了?而且数据库字段更干净,查询时不用联查多个字段,性能还能提升一点——我朋友的博客用户中心改完后,用户偏好的更新速度快了30%。
接口数据传递:让复杂结构“跨系统”不翻车
做接口的时候,你肯定遇到过“传递复杂数据”的需求:比如电商系统要给物流系统发订单信息,订单里有商品列表(数组)、用户信息(对象)、收货地址(关联数组),直接传数组或对象的话,对方系统可能解析错结构,比如物流系统用Java写的,看不懂PHP的对象格式。这时候序列化就是“翻译官”——把复杂结构转成字符串,对方收到后再“翻译”回去。
比如订单对象是这样的:
class Order {
public $orderId;
public $user; // User对象
public $products; // 商品数组
public $address; // 地址数组
public function __construct($id, $user, $products, $address) {
$this->orderId = $id;
$this->user = $user;
$this->products = $products;
$this->address = $address;
}
}
class User {
public $userId;
public $name;
public $email;
// 注意:这里有个敏感字段
private $passwordHash;
}
如果直接把Order
对象传给物流系统,对方可能读不到User
对象里的name
,或者敏感字段passwordHash
会泄露——去年我做电商接口时就踩过这个坑:一开始没过滤passwordHash
,序列化后传给物流系统,结果物流系统的日志里居然有用户的密码哈希,吓得我赶紧改代码。
怎么解决?用对象的魔术方法__sleep()
和__wakeup()
:__sleep()
决定哪些属性要被序列化,__wakeup()
在反序列化后恢复对象状态。比如给User
类加__sleep()
,过滤掉敏感字段:
class User {
public $userId;
public $name;
public $email;
private $passwordHash;
// 只序列化公开的、非敏感的属性
public function __sleep() {
return ['userId', 'name', 'email'];
}
}
这样序列化Order
对象时,User
里的passwordHash
就不会被包含进去,避免敏感信息泄露。
然后把Order
对象序列化后传给物流系统:
$order = new Order(123, $user, $products, $address);
$serializedOrder = serialize($order);
// 传给物流系统的接口(比如用curl)
curl_setopt($ch, CURLOPT_POSTFIELDS, ['data' => $serializedOrder]);
物流系统收到后,用unserialize()
恢复成Order
对象(前提是对方系统也用PHP,或者能解析PHP的序列化格式):
$serializedOrder = $_POST['data'];
$order = unserialize($serializedOrder);
// 现在能正常读取订单信息
echo $order->user->name; // 输出用户名
如果对方系统用Java或Python,怎么办?可以用JSON代替默认序列化——JSON是跨语言的,但缺点是不支持对象(只能转成数组)。比如用json_encode()
转成JSON字符串,对方用JSON.parse()
解析,不过如果你的数据里有对象,还是 用PHP的序列化,或者确保对方能处理对象结构。
缓存优化:把高频查询结果“压缩”存缓存
做高并发系统的时候,你肯定遇到过“数据库查询慢”的问题:比如首页的热销商品列表,每次刷新都要查数据库,联表、排序、计算销量,耗时0.5秒以上,用户等得不耐烦就走了。这时候序列化能帮你把查询结果“压缩”存到缓存里,下次直接取缓存,不用查数据库。
去年我帮一个生鲜电商做缓存优化时,就用了这个方法:首页的热销商品列表是查products
表和orders
表,计算近7天销量,然后排序取前10名,每次查询要0.5秒。后来我把查询结果序列化后存到Redis,过期时间设为5分钟,这样用户访问首页时,直接取Redis里的字符串,反序列化后显示,加载时间从0.5秒降到了0.01秒,用户反馈“首页快得像闪电”,转化率还提升了10%。
具体步骤:
php
$hotProducts = DB::table(‘products’)
->join(‘orders’, ‘products.id’, ‘=’, ‘orders.product_id’)
->select(‘products.*’, DB::raw(‘count(orders.id) as sales’))
->where(‘orders.created_at’, ‘>=’, now()->subDays(7))
->groupBy(‘products.id’)
->orderBy(‘sales’, ‘desc’)
->take(10)
->get()
->toArray();
转成字符串,存到Redis,设5分钟过期。
php
$serializedProducts = serialize($hotProducts);
Redis::set(‘home_hot_products’, $serializedProducts);
Redis::expire(‘home_hot_products’, 300); // 300秒=5分钟
php
$serializedProducts = Redis::get(‘home_hot_products’);
if ($serializedProducts) {
$hotProducts = unserialize($serializedProducts);
} else {
// 查数据库的代码(同上)
}
这里有个小技巧:用igbinary扩展代替默认的serialize(),能让序列化后的字符串小30%-50%,速度还快2-3倍——PHP官方文档也推荐用igbinary做高性能缓存(引用自PHP手册:https://www.php.net/manual/zh/book.igbinary.phpnofollow)。比如安装igbinary后,用
igbinary_serialize()和
igbinary_unserialize()代替默认函数,缓存体积更小,Redis的内存占用也更低。
不同序列化方式怎么选?一张表帮你理清楚
说了这么多,你可能会问:“默认serialize()、igbinary、JSON,到底选哪个?”我整理了一张表,帮你快速做决定:
序列化方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
默认serialize() | 兼容所有PHP版本,支持对象和复杂结构 | 字符串体积大,速度一般 | 简单场景(如用户偏好存储),不需要极致性能 |
igbinary | 体积小30%-50%,速度快2-3倍,支持对象 | 需要安装扩展(pecl install igbinary) | 高并发、大数量的缓存场景(如首页热销商品) |
JSON | 跨语言兼容(Java、Python都能解析),可读性好 | 不支持对象,复杂结构易出错 | 与非PHP系统交互的接口场景(如给Java系统传数据) |
比如你要给Java系统传数据,选JSON;要存缓存提升性能,选igbinary;只是简单存用户偏好,选默认serialize()就行。
最后再提醒你几个注意点:
或
LONGTEXT,Redis的value大小限制是512MB,别存太大的数据。
会返回
false,所以要加错误处理,比如:
php
$preferences = unserialize($user->preferences);
if ($preferences === false) {
// 恢复默认值或重新查询
$preferences = [‘color’ => ‘blue’, ‘font’ => ‘Arial’];
}
如果你按这3个场景试了,或者遇到什么问题,欢迎在评论区告诉我——我帮你看看哪里出问题了!毕竟序列化这东西,多练两次就顺手了,刚开始懵圈很正常,慢慢来~
本文常见问题(FAQ)
把用户偏好存数据库时,字段类型选什么好?
别选VARCHAR(255),很容易因为序列化后的字符串太长被截断,去年我朋友做博客用户中心时就踩过这坑——一开始用VARCHAR存偏好,结果数组稍大就被截了,反序列化直接报错。 用TEXT或LONGTEXT类型,能存更长的字符串,保证序列化数据完整。
接口传递对象时,怎么防止敏感字段泄露?
给对象加__sleep()魔术方法就行。比如User类里有passwordHash这种敏感字段,在__sleep()里返回需要序列化的属性数组(比如只返回userId、name、email),把敏感字段排除在外,这样序列化后的字符串就不会带密码哈希这些信息了,去年我做电商接口时就是这么处理的。
缓存高频查询结果时,序列化后的过期时间怎么定?
得看数据的更新频率。比如首页热销商品列表,近7天销量变化不算太快,设5-10分钟过期就行;如果是实时性很高的库存数据,可能要设1-2分钟。去年我帮生鲜电商优化时,把热销商品的缓存过期时间设为5分钟,既减少了数据库查询次数,又保证用户看到的不是太旧的数据。
默认serialize()、igbinary、JSON这三种序列化方式怎么选?
简单场景比如存用户偏好,用默认serialize(),兼容所有PHP版本,不用额外装东西;高并发、大数量的缓存场景(比如首页商品列表)选igbinary,体积比默认的小30%-50%,速度还快2-3倍,但要先通过pecl装扩展;和Java、Python等非PHP系统交互的话,选JSON,跨语言能解析,就是没法直接转对象,只能转数组。
反序列化时返回false怎么办?
说明序列化字符串可能被篡改或者截断了,得加错误处理。比如用unserialize()之后,判断结果是不是false,如果是的话,要么恢复默认值(比如用户偏好设成默认的蓝色、Arial字体),要么重新查数据库拿最新数据,别直接把错误抛给用户——去年我帮朋友调试时,就碰到过数据库字段被手动修改导致反序列化失败的情况,加了这步就稳了。