phpredis重试最佳实践
方案概述
phpredis是在PHP脚本中连接Redis较为常用的一个SDK,但是phpredis本身仅提供了基础连接和交互能力,在云上复杂网络场景可能丢包重传,或者在因硬件故障导致Redis主备切换等场景没有自动重连重试的能力。本文档将对此场景下PHP脚本如何进行可靠性改造提供思路和实践。
phpredis默认连接样例
如下样例使用phpredis的connect函数建立一个和Redis的长连接,然后在循环中不断对一个key值执行自增,操作间隔2秒。该简单程序样例无任何重连和重试机制,phpredis的connect函数本身也没有实现重连机制。在Redis发生主备切换的场景下,程序会直接进入异常,程序退出。
#!/usr/bin/env php
<?php
try {
$redis = new Redis();
// 连接Redis:127.0.0.1:6379,连接超时2s,重试间隔100ms
if (!$redis->connect("127.0.0.1", 6379, 2, null, 100)) {
throw new Exception("无法连接到Redis服务器");
}
// 删除现有键(确保从0开始计数)
$redis->del("redisExt");
echo "开始自增计数器 [按 Ctrl+C 退出]\n";
while (true) {
// 执行自增操作并获取新值
$newValue = $redis->incr("redisExt");
// 输出当前值和时间
printf("[%s] redisExt = %d\n",
date('Y-m-d H:i:s'),
$newValue);
// 等待2秒
usleep(2000000);
}
} catch (Exception $e) {
// 错误处理
echo "发生错误:\n";
var_dump($e);
exit(1);
}
优化思路及方案
默认情况下phpredis并不自动处理连接断开后的重连和重试,可以通过一些phpredis支持的配置来实现重连和重试机制。
实现phpredis重连的常见做法:
- 使用“pconnect”进行持久连接,但这并不能避免连接断开的情况(如Redis服务器重启、网络波动等)。
- 在发生连接异常时,捕获异常并尝试重新连接和重试操作。
phpredis中提供了如下参数,分别实现phpredis重连和重试的能力,详细说明请参见表1。
|
参数 |
说明 |
|---|---|
|
connect函数内置参数 |
connect函数的内置参数可以让连接拥有在断连时重连的能力。connect函数如下: $redis->connect(host, port, timeout, reserved, retry_interval, read_timeout, others); 其中各个参数说明如下:
|
|
OPT_MAX_RETRIES |
配置该参数后,当命令执行过程中遇到连接错误(如超时、连接中断)时,phpredis会立即重试该命令直到配置的最大次数。 注意该重试是自动进行的,由phpredis扩展在底层实现。重试的条件是特定的错误(通常是连接错误,如超时)。如果重试次数达到最大值仍然失败,则会抛出异常。 |
此外还需要设置合适的算法以及重试间隔,防止在特定批量断链场景下存在集中重试导致重试风暴,在少量断链场景下支持快速重试。phpredis中可以通过OPT_BACKOFF_ALGORITHM来设置退避算法,大部分场景下建议设置Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER,来随机化退避时延。
结合以上思路,可以得到优化后的如下样例。通过优化后的样例,样例程序在Redis操作异常(网络异常,主备倒换)时,仍然能够正常重试完成操作,同时在持续故障时提供清晰的错误信息和安全的退出机制。
#!/usr/bin/env php
<?php
try {
$redis = new Redis();
// 配置Redis连接选项
$redis->setOption(Redis::OPT_MAX_RETRIES, 3); // 设置最大重试次数
$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER); // 设置退避算法
$redis->setOption(Redis::OPT_BACKOFF_BASE, 100); // 基础延迟100ms
$redis->setOption(Redis::OPT_BACKOFF_CAP, 2000); // 最大延迟2000ms
// 连接Redis:127.0.0.1:6379,连接超时2秒,自动重试,重试间隔100ms
if (!$redis->connect("127.0.0.1", 6379, 2, '', 100)) {
throw new Exception("无法连接到Redis服务器");
}
// 删除现有键(确保从0开始计数)
$redis->del("redisExt");
echo "开始自增计数器 [按 Ctrl+C 退出]\n";
$lastSuccess = time();
$consecutiveFails = 0;
while (true) {
try {
// 执行自增操作并获取新值
$newValue = $redis->incr("redisExt");
// 重置失败计数
$consecutiveFails = 0;
$lastSuccess = time();
// 输出当前值和时间
printf("[%s] redisExt = %d\n",
date('Y-m-d H:i:s'),
$newValue);
// 正常等待2秒
usleep(2000000);
} catch (RedisException $e) {
// 处理连续失败
$consecutiveFails++;
// 输出错误信息
printf("[%s] 错误: %s (连续失败 %d 次)\n",
date('Y-m-d H:i:s'),
$e->getMessage(),
$consecutiveFails);
// 指数退避等待:100ms, 200ms, 400ms, 800ms...
$waitTime = min(100 * pow(2, $consecutiveFails), 5000); // 最大5秒
usleep($waitTime * 1000);
// 连续失败超过阈值后尝试完全重连
if ($consecutiveFails >= 5) {
echo "尝试完全重新连接...\n";
try {
$redis->close();
$redis->connect("127.0.0.1", 6379, 2);
$consecutiveFails = 0; // 重置失败计数
echo "重新连接成功\n";
} catch (Exception $reconnectEx) {
echo "重新连接失败: " . $reconnectEx->getMessage() . "\n";
}
}
// 长时间无成功操作时强制退出
if ((time() - $lastSuccess) > 60) {
throw new Exception("超过60秒无成功操作,终止脚本");
}
}
}
} catch (Exception $e) {
// 错误处理
echo "\n发生错误:\n";
echo "[" . date('Y-m-d H:i:s') . "] " . $e->getMessage() . "\n";
// 尝试记录最终计数器值
try {
if (isset($redis) && $redis->isConnected()) {
$finalValue = $redis->get("redisExt");
echo "最终计数器值: " . ($finalValue ?: 'N/A') . "\n";
}
} catch (Exception $finalEx) {
echo "获取最终值失败: " . $finalEx->getMessage() . "\n";
}
exit(1);
}