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