更新时间:2025-08-07 GMT+08:00
分享

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重连的常见做法:

  1. 使用“pconnect”进行持久连接,但这并不能避免连接断开的情况(如Redis服务器重启、网络波动等)。
  2. 在发生连接异常时,捕获异常并尝试重新连接和重试操作。

phpredis中提供了如下参数,分别实现phpredis重连和重试的能力,详细说明请参见表1

表1 phpredis重连和重试参数

参数

说明

connect函数内置参数

connect函数的内置参数可以让连接拥有在断连时重连的能力。connect函数如下:

$redis->connect(host, port, timeout, reserved, retry_interval, read_timeout, others);

其中各个参数说明如下:

  • host:Redis地址的字符串。可以是IP,或 Unix socket的路径。从 5.0.0 版本开始可指定协议方案(schema)。
  • port:Redis端口,类型为整型,可选。默认为6379。
  • timeout:连接超时时间,类型为浮点数,单位为秒(可选,默认值0表示使用系统默认socket超时时间)。
  • reserved:当指定 retry_interval 时必须设为空字符串:''。
  • retry_interval:重试间隔,类型为整型,单位为毫秒(可选)。
  • read_timeout:获取Redis命令返回超时时间,类型为浮点数,单位为秒(可选,默认值0表示使用系统默认socket超时时间)。
  • others:数组,在phpredis≥5.3.0的版本中允许设置认证(auth)和流(stream)配置。

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

相关文档