第一章:高并发基石: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的哪种数据结构来管理库存?为什么?