1. 两种主流扩展与选型:第一次选错后面都要还债
1.1 phpredis 和 Predis,本质上是两条完全不同的路
PHP 连 Redis 这件事,绕来绕去其实最后就是两条路:扩展版 phpredis,和纯 PHP 实现的 Predis。很多新手一开始不理解为什么要有两种,直接用哪个都行,等到踩坑之后才发现,选型不是随心情,而是要看清自己的运行环境、部署方式、以及团队维护能力。
phpredis 是 C 写的扩展,装进 PHP 进程里之后,你的 PHP 里直接多出一批类和方法:Redis、RedisCluster、RedisSentinel。它走的是扩展内部直接跟 Redis Server 交互的路子,性能高,函数覆盖面广,长期跑实际项目的团队大多数都会偏向它。
Predis 则是一个 Composer 依赖包,不依赖任何扩展,只要 PHP 能跑,就能用它连 Redis。它适合那种没有服务器权限、不能装扩展的虚拟主机环境,或者你希望把 Redis 相关的依赖全部锁在composer.json里,不跟 PHP 环境绑定。
先看最基础的连接写法,后面所有复杂配置都从这里长出来:
// phpredis 扩展方式 $redis = new Redis(); $redis->connect('127.0.0.1', 6379, 2.5); $redis->auth('your_password'); $redis->select(0);// Predis 依赖方式 require 'vendor/autoload.php'; $client = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'password' => 'your_password', 'database' => 0, ]); $client->set('name', 'phper');单看这段代码,你觉得区别不大。但真正压测起来,phpredis 的吞吐和响应延迟账面数据会比 Predis 好看不少,特别是在并发高、单个请求操作次数多的系统里。Predis 也并不是慢到不能用,它的问题更多是内存占用和 CPU 开销偏高,因为每次命令封装解析都是 PHP 代码在跑。
1.2 选型时真正要看的几个维度
第一看环境可控性。如果你用的是裸机、容器、云主机,能自由装扩展,那直接 phpredis。如果你要交付给一个不能动环境、只能用 Composer 拉包的项目,那 Predis 是唯一不给自己惹麻烦的选择。我见过不少同学为了性能强行编译 phpredis,结果目标服务器上缺编译工具链,部署流程变得特别脆弱,最后又改回 Predis。
第二看协议演进。Redis 不断在更新,RESP3、ACL、Stream、Lua 脚本事务、集群功能都在变。phpredis 保持 C 扩展同步更新比较勤快,新协议和新命令的跟进速度更快。Predis 当然也有语义覆盖,但某些新命令的支持可能会有滞后,尤其是你们团队喜欢尝鲜用新特性的时候,这一点会非常明显。
第三看维护成本。phpredis 虽然性能好,但版本和 PHP 版本之间是有兼容性的。PHP 8.1 和 8.2 上了之后,你要记得把 phpredis 版本同步升级,否则可能出现启动报错。Predis 则没有这个麻烦,只要 Composer 能解析依赖就行。
我给出一个我自己常用的对比思路,不写太多虚的,直接看表:
| 对比项 | phpredis | Predis |
|---|---|---|
| 实现语言 | C 扩展 | 纯 PHP |
| 安装方式 | PECL / 编译 | Composer |
| 性能 | 高,命令执行开销极低 | 中下,适合低并发场景 |
| 部署约束 | 需要控制 PHP 环境 | 只要能用 PHP 就行 |
| 功能覆盖 | 新命令支持快 | 基础能力全,新功能滞后 |
| 集群/哨兵 | 有 RedisCluster、RedisSentinel | 通过配置项支持 |
| 长连接 | 支持 pconnect | 支持 persistent |
| 适用场景 | 高并发、自建服务、容器化 | 受限主机、快速接入 |
1.3 小团队真正该记住的选型结论
如果你只为服务端做个缓存读写,项目规模不大,用户量也就每秒几百个请求,Predis 完全够用。不要因为网上都说“性能差距很大”就恐慌,实际上很多系统的瓶颈在数据库和业务逻辑,不在这一层 Redis 客户端。
但如果你已经在做万人级在线应用、消息队列、秒杀扣减、或者 Redis 承载了核心业务数据,别犹豫,直接用 phpredis。原因不只是快,而是它在长期跑高并发时更稳,出错也更直观,起码你能用pecl管版本,也能通过php -m确认加载状态。
2. 从零安装 Redis 扩展:PECL、源码编译与容器化三条路线
2.1 PECL 安装与源码编译的完整流程
很多教程一上来就让人编译源码,其实大部分场景根本不用走到那一步。只要你的 PHP 环境里带了pecl,一条命令就能装好:
pecl install redis如果默认源下载慢或者版本冲突,可以指定版本:
pecl install redis-6.1.0装完需要在php.ini里开启扩展。Linux 环境下通常在扩展目录生成redis.so,然后在 php.ini 加一行:
extension=redis.so如果你需要定制编译参数,再走源码路线。比如项目里大量使用 igbinary 做序列化、想开启 LZF 压缩,源码编译时就要显式开启这些选项:
cd phpredis phpize ./configure --enable-redis-igbinary --enable-redis-lzf --enable-redis-zstd make -j$(nproc) make install这里我解释一下为什么要开这些参数。--enable-redis-igbinary让 phpredis 支持 igbinary 序列化协议,它比默认的 serialize 更快、生成的数据更小。--enable-redis-lzf和--enable-redis-zstd是压缩支持,适合存大字符串的场景。如果不开,后面你在代码里写Redis::SERIALIZER_IGBINARY也不会生效,甚至可能报错。
2.2 Docker 环境下安装,别再手动进容器搞编译
容器化部署已经是主流,最省心的方式是把扩展装进基础镜像,构建镜像时一次性装好:
FROM php:8.2-cli RUN pecl install redis && docker-php-ext-enable redis CMD ["php", "-m"]这里有一个坑:很多同学习惯在容器启动后手动执行docker-php-ext-install redis,但你会发现官方 PHP 镜像根本没有 redis 源码包,这个命令会提示找不到。正确做法是用pecl install redis安装源码包,然后用docker-php-ext-enable redis生成并启用配置。docker-php-ext-enable是镜像内置工具,它会自动找到redis.so并写入对应 ini 文件。
如果你用的是 swoole、workerman 这类常驻内存框架,也是同样套路,基础镜像里装好 redis 扩展即可,不要每次启动都从网络拉取安装。
2.3 装完必须验证,别急着写业务代码
扩展装完,一定要回到 PHP 层验证一次。最直接的一句话:
php -m | grep redis有输出说明扩展已经加载。如果想看版本和配置细节:
php -i | grep -i redis代码层面也可以验证:
<?php var_dump(extension_loaded('redis')); var_dump(Redis::VERSION); $redis = new Redis(); var_dump($redis->connect('127.0.0.1', 6379, 1.0)); var_dump($redis->ping());ping()返回+PONG字符串或者true,说明 PHP 与 Redis 的整个链路已经通。这一小步看起来多余,但能在后续排查连线问题时明确一个边界:扩展本身没问题,问题出在连接参数、网络、权限、或者 Redis 服务。
3. PHP 连接 Redis 的四种典型场景:单机、Socket、哨兵、集群
3.1 单机 TCP 连接与基础参数解析
单机 TCP 是最常见的连接方式,也是最容易被人轻视的。很多人只知道填 host 和 port,超过 config 文档里面写的参数根本不去看,后面一遇到超时就成了当场的经验。
$redis = new Redis(); $redis->connect( '127.0.0.1', 6379, 3.0, // 连接超时:单位秒,支持小数 null, // reserved 参数,保留,默认 NULL 200, // retry_interval:重试间隔,单位毫秒 3.0 // read timeout:读取超时,单位秒 );这里我逐个说清楚:
- 第三个参数是连接超时,单位秒,可以传
0.5这种小数。如果 Redis 服务端心跳不在,PHP 会卡在 TCP 建立阶段,这个参数就是上限。 - 第四个参数
reserved在老版本里是给重试用的,新版本大多传NULL。 - 第五个参数是重试间隔,单位毫秒,只有在超时后才生效。
- 第六个参数是读取超时,很多糊涂的超时都跟它有关。你写一个耗时很长的 Lua 脚本,如果读取超时设得太短,Redis 本身还在执行,PHP 这边已经抛一句 “Redis server went away” 给你。
如果开启了 Redis 密码验证,连接后要马上执行auth,或者在新版本 phpredis 的构造函数里直接带上认证信息。
3.2 Unix Socket 连接:同机部署时被低估的方案
Redis 和 PHP 在同一台机器时,用 TCP 永远不是性能最优解,哪怕走 loopback 也绕不过完整的 TCP 协议栈。更狠的做法是让 Redis 监听 Unix Socket,PHP 直接连 Socket 文件。
先改 Redis 配置:
port 0 unixsocket /var/run/redis/redis.sock unixsocketperm 700PHP 侧连接:
$redis = new Redis(); $redis->connect('/var/run/redis/redis.sock');你注意,不使用 TCP 时,host 位置直接传 Socket 文件路径,port 传 0 或者不传。
这个方案的收益很明显:少一层网络协议解析,同一台机器上大并发请求 Redis 时,RT 能往下压。代价也明显:Socket 文件需要在 PHP-FPM 进程和 Redis 进程之间共享权限,容器场景尤其麻烦。你在 Docker 里跑两个容器,一个 PHP 一个 Redis,要么共享 volume,要么还是老实走 TCP。
3.3 哨兵模式和集群模式连接:别再用单机客户端去连
生产环境一旦上了哨兵或集群,代码就再也不是new Redis()加一个 host 那么简单。
哨兵模式客户端要做的事情是:先找到哨兵节点,询问当前主节点是谁,再连接真实主节点。phpredis 提供了原生RedisSentinel类,示例:
$sentinel = new RedisSentinel('127.0.0.1', 26379); $master = $sentinel->getMasterAddrByName('mymaster'); // $master = ['127.0.0.1', 6379] $redis = new Redis(); $redis->connect($master[0], $master[1]);Predis 则可以通过replication配置直接实现哨兵,这里不展开,因为我自己实际项目中更倾向 phpredis。
Redis Cluster 就更讲究了。它把数据分槽到多个节点,客户端必须处理MOVED重定向。直接用单机Redis客户端连接集群节点,打开写入大概率报错。phpredis 提供了专用的RedisCluster:
$cluster = new RedisCluster( null, ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7002'], 1.5, 0, false, 'cluster_password' ); $cluster->set('user:1001', 'name');构造参数第二项是种子节点列表,客户端内部会自动拉取集群拓扑。后面只要不换库,正常set、get都会自动定位到正确的槽位节点。
3.4 连接池与持久连接的取舍,别把 pconnect 当连接池
一说到连接池,有人第一反应是pconnect。严格讲,pconnect是持久连接,不是连接池。
phpredis 的pconnect底层效果是这样的:一个 PHP-FPM worker 进程第一个请求连接 Redis 之后,这个 socket 在 worker 进程内被保存下来,后续这个 worker 处理的请求复用同一个 socket,不再重新建连。不同的 worker 进程之间不会共享。
问题来了:PHP-FPM 会动态 fork 大量 worker,跑一段时间后 Redis 的connected_clients数字可能长得比你想象中高,每个 worker 都握着一根连接,但这并不等于你拥有一个智能连接池。Redis 服务端如果设置了timeout,空闲连接会被断开,但 worker 并不知道,下一次请求拿旧 socket 去发命令,就会报连接失效。所以用pconnect必须搭配重连判断机制。
真正的连接池,更适合 Swoole、Workerman 这种常驻进程场景。连接池的核心是把连接对象放进一个池子,请求来的时候借,用完了还,不够再创建。我用 Swoole 的 Channel 做简单示意:
$pool = new Swoole\Coroutine\Channel(10); for ($i = 0; $i < 10; $i++) { $pool->push(createRedisConnection()); } $redis = $pool->pop(); try { $redis->set('foo', 'bar'); } finally { $pool->push($redis); }这里看起来简单,但实际要处理 Redis 连接失效、自动重建、归还时校验、协程上下文分离等问题。如果你们项目没有这么高的并发需求,别过早引入连接池。
4. 连接参数与重试策略:不要只填 host 和 port
4.1 常用的 setOption 配置,每一个都有实际价值
很多项目的 Redis 连接代码写得极其朴素:connect、set、get、没有了。但 phpredis 提供的setOption里,有几个选项会影响你后面是否踩坑。
$redis = new Redis(); $redis->setOption(Redis::OPT_READ_TIMEOUT, 3.0); $redis->setOption(Redis::OPT_TCP_KEEPALIVE, 1); $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); $redis->setOption(Redis::OPT_PREFIX, 'app:');OPT_READ_TIMEOUT设置读取超时,对应前面的第六个连接参数,但这里可以随时动态调整。OPT_TCP_KEEPALIVE开启 TCP 保活,能让网络异常连接更快被发现。OPT_SERIALIZER决定数据写入 Redis 前怎么编码,用 igbinary 或者 JSON 都比默认 serialize 省空间。OPT_PREFIX则给 key 加统一前缀,特别适合多业务共存一个 Redis 实例的隔离场景。
需要注意的是,这些选项不是连上之后永久生效的。每次 new Redis 对象都要重新设置。如果你用了pconnect,理论上复用同一个连接,但 PHP 层面的对象每次请求还是重新初始化的,所以这些 option 仍然要写在连接初始化代码里。
4.2 超时和重试,不要傻乎乎地无限重试
一个典型错误:
$redis = new Redis(); $redis->connect('10.0.0.8', 6379, 3.0);如果 Redis 网段故障,这个 connect 阻塞 3 秒才会失败。业务请求一堆,全部卡在这 3 秒,后面 FPM 进程池直接被打满,后果比 Redis 挂掉更严重。
我建议的写法是:连接超时控制在 1 秒以内,失败快速返回,不做无谓重试;如果要做重试,用退避方式,并且限制次数:
$maxRetry = 3; $retryDelay = 50; // 初始 50ms for ($i = 0; $i < $maxRetry; $i++) { try { $redis = new Redis(); $redis->connect('10.0.0.8', 6379, 1.0); $redis->ping(); break; } catch (RedisException $e) { usleep($retryDelay * 1000); $retryDelay *= 2; } }这种退避策略能避免 Redis 短暂故障时,所有请求在同一瞬间重连打爆服务。另外,不要用 Redis 来当请求熔断器之外的唯一依赖,如果 Redis 不可用,要提前想好业务降级方案,比如读取本地文件缓存,或者直接返回降级数据。
4.3 认证与库选择:连接之后的第一件事
Redis 6 之后 ACL 用得越来越多,密码认证不再只是requirepass一个全局密码。phpredis 里auth可以传入用户名和密码:
$redis->auth(['user', 'password']);选库要小心,select切换实际是修改连接上下文,并不是每条命令单独指定库。你用pconnect时,一个 worker 进程可能第一次请求用了select(0),第二次请求用了select(1),第三次请求没有select,PHP 侧只负责提供服务,不会帮你重置回 0。这就是长连接状态下最常见的问题:拿到预期的 connection,但库不对,读取数据全是空。
我的做法是:使用长连接时,每次请求开始强制select目标库,或者直接用带前缀的 key 与单库设计,少用 select 切库。业务一旦设计成多库依赖,排查成本会指数上升。
5. 连接排查实录:先从 redis-cli 开始,再回到 PHP 调用
5.1 一个标准的排查顺序
遇到 PHP 连接 Redis 报错,不要第一反应就改业务代码。我按下面这个顺序从底往上排查,基本能覆盖 90% 的问题:
第一步,确认 Redis 服务本身健康:
redis-cli -h 127.0.0.1 -p 6379 ping如果这一步都失败,说明问题就在 Redis 进程、防火墙、端口监听或者 Redis 配置的 bind 地址上。
第二步,确认 Redis 允许客户端来源。Redis 默认可能只监听127.0.0.1,PHP 部署在另一台机器时,必须监听内网 IP,并且设置好密码。
第三步,确认 PHP 环境能连 Redis。直接跑一段最基础的代码,不要带任何框架初始化:
php -r "new Redis()->connect('127.0.0.1', 6379); echo 'ok';"这一步能快速区分是框架问题还是连接配置问题。
第四步,查系统连接状态:
ss -tn | grep 6379看 TCP 连接是 ESTABLISHED 还是 SYN_SENT。如果是 SYN_SENT,说明网络层到不了 Redis。如果看到大量 TIME_WAIT,则说明你的 PHP 请求经常建立新连接,可以考虑pconnect或连接池。
5.2 序列化类型不对,排查起来最头疼
有一种非常隐蔽的坑:代码里写入数据时设置了某一种序列化方式,读取时却换了一种。比如你用Redis::SERIALIZER_JSON写入一个键,后来有人改了配置用默认 PHP serialize 读,直接解析不出来,或者拿到的字符串和预期结构完全对不上。
我在实际项目里用统一封装的 Redis 服务类,把 setOption 固定写在构造方法里,并要求所有调用方只能通过封装类访问 Redis,禁止裸 new。这样能从源头避免序列化混乱。
5.3 本机连接 Redis 很稳,线上却频繁断
这种问题十有八九和连接保活、Redis 超时配置、以及网络设备有关。
线上环境如果在 Docker 里,宿主机的连接超时会被 Docker 网关的 iptables 规则影响。长连接一段时间没有数据传输,中间设备把连接静默回收,PHP 端感知不到,下次用旧连接访问就失败。解决思路有两个方向:一是开启OPT_TCP_KEEPALIVE和内核 keepalive,主动探测连接是否还活着;二是在重新使用长连接前先ping一下,失败就 reconnect。
我推荐先做 ping 检查,因为这不仅能防网络设备回收,还能防 Redis 服务端主动断掉空闲连接。ping 失败的处理逻辑并不复杂,把重连代码统一封装一次,所有地方复用。
6. 常见坑位速查:这些报错我基本都见过
6.1 异常信息速查表
| 报错关键字 | 常见原因 | 对应处理 |
|---|---|---|
| Connection refused | 端口没监听、服务没启动、防火墙拦、bind 地址不对 | 先ss -lntp看端口,再redis-cli ping |
| Redis server went away | 服务重启、连接被服务端断开、旧的持久连接失效 | 重新 new Redis,做 ping 检查后重连 |
| NOAUTH Authentication required | 没有给密码,或者不是 ACL 指定用户密码 | 调用auth,检查密码和用户名 |
| WRONGPASS invalid username-password pair | 账号密码错误 | 检查 ACL 配置和连接参数 |
| READONLY You can't write against a read only replica | 客户端连到了从节点,却发写命令 | 读操作走从,写操作走主 |
| MOVED 127.0.0.1:7001 | 单节点连接收到集群重定向 | 用RedisCluster连接集群 |
| Connection timed out | 网络不通、防火墙丢包、连接超时太短 | 调connect_timeout,检查网络路由 |
| ERR unknown command | Redis 版本低、命令被 rename、权限受限 | 对比 Redis 版本,检查配置文件 |
6.2 我踩过的几个认知误区
第一个误区是“只要 redis-cli 能连,PHP 就一定能连”。redis-cli 通常跑在 Redis 所在机器上,走的是本地回环地址;PHP 可能在另一台机器,或者容器内。两地网络策略完全不同,必须站在 PHP 所在环境去测试。
第二个误区是把pconnect当成灵丹妙药。FPM 模式下,它对单机连接数压力有缓解,但无法真正按需复用和回收,项目连接数依然不可控。
第三个误区是忽略 Redis 配置里的timeout和tcp-keepalive。服务端认为自己断掉一个空闲连接是正常的,但客户端如果意识不到,下一个请求就会报错。最好的办法是客户端每次使用长连接前做一次轻量ping,发现不通就重建连接。
第四个误区是连接超时和读写超时混为一谈。connect的第三个参数只管建连阶段,一旦建连成功,慢命令会卡在读取阶段。如果你执行一个耗时的 Lua 脚本或者keys *大扫描,读写超时设得太短就会让你误以为连接断了。
6.3 一个省心的小习惯:连接代码永远单独封装
我最后再分享一个实战中很值得坚持的小习惯。不要让业务代码里到处散落new Redis()、connect、auth、select,而是封装一个RedisClient工厂:
class RedisClient { public static function create(): Redis { $redis = new Redis(); $redis->connect('127.0.0.1', 6379, 1.0); $redis->auth('your_password'); $redis->select(0); $redis->setOption(Redis::OPT_READ_TIMEOUT, 3.0); $redis->setOption(Redis::OPT_TCP_KEEPALIVE, 1); return $redis; } }这样做的价值不是代码少写几行,而是当 Redis 从单机升级到哨兵、再到集群时,你只需要改动一个工厂方法,不用把几十处连接代码翻出来逐个改。我经历过一次项目从单机架构切到集群架构,那时候特别庆幸连接代码早就做了统一封装。这也是这一篇实战实录最想给你留下的建议:连接方案会变,统一入口不变,后面所有优化都有发力点。