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); }