第一章:高并发基石:Redis核心数据结构与PHP客户端选型
各位同学,咱们今天聊聊高并发的第一块基石——Redis。说实话,我见过太多项目,Redis用得挺溜,但一问到为什么选这个数据结构,就支支吾吾了。这不行。你想想看,高并发场景下,选错一个数据结构,可能就是几毫秒和几百毫秒的差距。
1.1 先说说Redis到底是个啥
Redis,全称是Remote Dictionary Server。说白了,就是一个跑在内存里的键值对数据库。我习惯把它当成一个超级大的HashMap,只不过这个HashMap是线程安全的,而且支持各种花里胡哨的数据结构。
为什么高并发离不开它?因为内存操作的速度,是磁盘的十万倍以上。你想想看,MySQL查一条数据可能要10ms,Redis只要不到1ms。这差距,在高并发下就是天壤之别。
核心要点:Redis的所有操作都是原子性的。这意味着,多个客户端同时操作同一个key,不会出现数据错乱。我在项目中遇到过,有人用文件锁做并发控制,结果性能惨不忍睹。换成Redis的原子操作后,问题迎刃而解。
1.2 五种核心数据结构,你得烂熟于心
Redis有五种基本数据结构,我一个个说。每个我都会结合项目经验,告诉你什么时候用,什么时候千万别用。
1.2.1 String(字符串)—— 最基础,也最常用
String是Redis最基础的数据结构。它可以存字符串、数字、甚至是二进制数据。最大512MB。
// 设置一个key
$redis->set('user:1:name', '张三');
// 获取
$name = $redis->get('user:1:name');
// 自增操作,高并发下非常有用
$redis->incr('page_view_count');
我常用的场景:
- 缓存用户信息、配置项
- 计数器(比如文章阅读量、点赞数)
- 分布式锁(配合SETNX命令)
小技巧:用String做计数器时,记得用incr而不是先get再set。incr是原子操作,不会出现并发问题。我曾经见过有人用get+set做计数器,结果在高并发下数据对不上,排查了半天才发现是这个问题。
1.2.2 List(列表)—— 消息队列的好帮手
List是一个双向链表。可以从左边或者右边插入、弹出元素。说白了,就是你可以把它当队列用,也可以当栈用。
// 从右边插入(当作队列)
$redis->rpush('task_queue', 'task1');
$redis->rpush('task_queue', 'task2');
// 从左边弹出
$task = $redis->lpop('task_queue');
避坑指南:我曾经用List做消息队列,结果消费者处理速度跟不上生产者,List越积越长,最后内存爆了。后来我改用BRPOP命令,设置超时时间,才解决了这个问题。记住,List做队列时一定要考虑消费速度。
1.2.3 Set(集合)—— 去重和交集的好手
Set是无序的字符串集合,元素不能重复。支持交集、并集、差集操作。
// 添加元素
$redis->sadd('user:1:tags', 'PHP', 'Redis', '高并发');
// 判断是否存在
$exists = $redis->sismember('user:1:tags', 'PHP');
// 获取交集
$common = $redis->sinter('user:1:tags', 'user:2:tags');
我常用的场景:
- 标签系统(比如给文章打标签)
- 共同好友(用交集实现)
- 抽奖去重(用Set保证一个人只能中一次)
1.2.4 Hash(哈希)—— 对象存储的首选
Hash是一个键值对集合,适合存储对象。比如用户信息,用Hash存比用String存更省内存,而且可以单独更新某个字段。
// 设置用户信息
$redis->hset('user:1', 'name', '张三');
$redis->hset('user:1', 'age', 25);
$redis->hset('user:1', 'email', 'zhangsan@example.com');
// 获取所有字段
$user = $redis->hgetall('user:1');
注意:Hash虽然好用,但不要存太多字段。我见过有人一个Hash里存了上千个字段,结果每次操作都慢得要命。一般来说,一个Hash的字段数控制在100以内比较合适。
1.2.5 Zset(有序集合)—— 排行榜的利器
Zset和Set类似,但每个元素都有一个分数(score),Redis会根据分数自动排序。说白了,就是带权重的Set。
// 添加元素,分数是100
$redis->zadd('leaderboard', 100, 'user1');
$redis->zadd('leaderboard', 200, 'user2');
// 获取排名前10的用户
$top10 = $redis->zrevrange('leaderboard', 0, 9, true);
我常用的场景:
- 实时排行榜(比如游戏积分榜)
- 延迟队列(用时间戳作为分数)
- 限流(用滑动窗口实现)
1.3 PHP客户端选型,我踩过的坑
PHP连接Redis,主要有三个客户端:predis、phpredis、以及最新的phpredis-igbinary。我一个个说。
| 客户端 | 类型 | 性能 | 特点 |
|---|---|---|---|
| predis | 纯PHP实现 | 较低 | 无需安装扩展,兼容性好 |
| phpredis | C扩展 | 高 | 性能好,功能全 |
| phpredis-igbinary | C扩展+序列化优化 | 最高 | 内存占用更小,速度更快 |
我的建议:
- 如果是小项目,或者不方便安装扩展,用predis就行。但要注意,predis是纯PHP实现,高并发下性能会成问题。
- 生产环境,我强烈推荐phpredis。性能差距不是一星半点。我记得有一次,把predis换成phpredis后,同样的代码,QPS从2000涨到了8000。
- 如果对性能有极致要求,可以试试phpredis-igbinary。它用了更高效的序列化方式,内存占用能减少30%左右。
避坑指南:我曾经在生产环境用predis,结果遇到高并发时,PHP进程全部卡在Redis操作上,CPU飙升到100%。后来排查发现,predis的socket连接没有做超时处理。换成phpredis后,配合连接池,问题彻底解决。
1.4 连接池,高并发的命门
很多新手会犯一个错误:每次请求都新建一个Redis连接。你想想看,建立TCP连接的开销有多大?在高并发下,这简直就是灾难。
// 错误示范:每次请求都新建连接
function getUser($id) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
return $redis->get("user:$id");
}
// 正确示范:使用连接池
class RedisPool {
private static $instance;
public static function getInstance() {
if (!self::$instance) {
self::$instance = new Redis();
self::$instance->connect('127.0.0.1', 6379);
self::$instance->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
}
return self::$instance;
}
}
// 使用
$redis = RedisPool::getInstance();
$user = $redis->get("user:$id");
小技巧:用phpredis时,可以开启长连接。在connect时加上超时参数,比如connect('127.0.0.1', 6379, 2.5),这样连接会复用,性能提升明显。
1.5 序列化,一个容易被忽视的细节
Redis存的是字符串,但PHP的数据结构是数组、对象。怎么转换?序列化。我见过有人直接用serialize(),结果存进去的数据又大又慢。
我的建议:
- 简单数据用JSON序列化,可读性好,跨语言兼容
- 复杂对象用phpredis自带的序列化器(Redis::SERIALIZER_PHP)
- 追求极致性能,用igbinary
// 推荐方式:用JSON
$user = ['name' => '张三', 'age' => 25];
$redis->set('user:1', json_encode($user));
// 读取时反序列化
$user = json_decode($redis->get('user:1'), true);
// 或者用phpredis的序列化器
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$redis->set('user:1', $user); // 自动序列化
注意:不要用PHP的serialize()直接存。它生成的字符串包含类名信息,如果类名变了,反序列化会失败。我吃过这个亏,后来全改成JSON了。
1.6 本章小结
嗯,这一章的内容就这些。说白了,Redis的核心就是那五种数据结构,选对数据结构,你的高并发就成功了一半。PHP客户端选型上,我建议生产环境用phpredis,配合连接池和合适的序列化方式,性能绝对够用。
下一章,咱们聊聊Redis的持久化策略。这可是个坑,我当年差点因为这个把数据全丢了。到时候细说。
课后思考:如果你现在要设计一个秒杀系统,你会用Redis的哪种数据结构来管理库存?为什么?