4. 重试策略基础:固定间隔重试、指数退避、随机退避、线性退避

好,咱们进入重试策略的核心部分。

说实话,我在做OTA升级的早期,对重试策略的理解特别简单——失败了就等一会儿再试呗。结果呢?有一次量产设备大规模升级,服务器直接被冲垮了。嗯,那次教训挺深刻的。

重试策略,说白了就是决定「失败后等多久再试」的一套规则。别小看这个「等多久」,选错了,轻则升级慢,重则把服务器搞崩。

4.1 固定间隔重试

这是最朴素的做法。每次失败后,等待固定的时间,比如30秒,然后重试。

核心逻辑: 等待时间 = 常量

// 固定间隔重试示例
#define RETRY_INTERVAL_MS 30000  // 30秒

int ota_retry_fixed_interval(int max_retries) {
    for (int i = 0; i < max_retries; i++) {
        int result = ota_download_chunk();
        if (result == SUCCESS) {
            return SUCCESS;
        }
        printf("第%d次失败,等待%d秒后重试\n", i+1, RETRY_INTERVAL_MS/1000);
        delay_ms(RETRY_INTERVAL_MS);
    }
    return FAILED;
}

优点: 实现简单,逻辑清晰。适合网络状况相对稳定的场景。

缺点: 太死板了。你想想看,如果网络正在拥堵,你每隔30秒捅一刀,服务器根本缓不过来。

我的经验: 固定间隔我只在内部局域网升级时用过。外网环境?我建议你慎重。我曾经在一个项目里用固定间隔重试,结果网络抖动时,所有设备同时重试,把服务器打出了502。

4.2 指数退避

这才是工业界的主流做法。每次重试的等待时间,按指数增长。

核心逻辑: 等待时间 = 基数 × 2^重试次数

// 指数退避重试示例
#define BASE_DELAY_MS 1000  // 基数1秒

int ota_retry_exponential_backoff(int max_retries) {
    int delay = BASE_DELAY_MS;
    for (int i = 0; i < max_retries; i++) {
        int result = ota_download_chunk();
        if (result == SUCCESS) {
            return SUCCESS;
        }
        printf("第%d次失败,等待%dms\n", i+1, delay);
        delay_ms(delay);
        delay *= 2;  // 每次翻倍
        if (delay > MAX_DELAY_MS) {
            delay = MAX_DELAY_MS;  // 设置上限
        }
    }
    return FAILED;
}

举个例子:基数是1秒,第一次失败等1秒,第二次等2秒,第三次等4秒,第四次等8秒……

为什么要这样?你想啊,网络出问题的时候,往往不是单个设备的问题。如果所有设备都按指数退避,越往后重试的设备越少,服务器就有喘息的机会。

注意: 一定要设置最大延迟上限。我见过有人没设上限,结果重试到第10次,等了512秒,用户早把设备砸了。一般建议上限设在30秒到120秒之间。

4.3 随机退避

指数退避虽然好,但有个问题——如果多个设备同时开始重试,它们的重试时间点可能还是撞在一起。

随机退避就是来解决这个问题的。在等待时间上加上一个随机因子。

核心逻辑: 等待时间 = 基础时间 + random(0, 抖动范围)

// 随机退避重试示例
#include <stdlib.h>

int ota_retry_random_backoff(int max_retries) {
    srand(get_device_id());  // 用设备ID做种子,避免所有设备随机数一样
    for (int i = 0; i < max_retries; i++) {
        int result = ota_download_chunk();
        if (result == SUCCESS) {
            return SUCCESS;
        }
        int base_delay = 1000 * (1 << i);  // 指数部分
        int jitter = rand() % 1000;         // 0~1秒的随机抖动
        int delay = base_delay + jitter;
        if (delay > MAX_DELAY_MS) {
            delay = MAX_DELAY_MS;
        }
        printf("第%d次失败,等待%dms\n", i+1, delay);
        delay_ms(delay);
    }
    return FAILED;
}

说白了,就是在指数退避的基础上,加了个「随机数」来打散重试时间点。

避坑指南: 我曾经犯过一个错——直接用rand()不加种子。结果所有设备复位后随机数序列一模一样,等于没加随机。后来我用设备MAC地址的后几位做种子,才真正把时间点打散。

4.4 线性退避

线性退避介于固定间隔和指数退避之间。每次重试,等待时间按固定步长增加。

核心逻辑: 等待时间 = 初始值 + 步长 × 重试次数

// 线性退避重试示例
#define INITIAL_DELAY_MS 5000   // 初始5秒
#define STEP_DELAY_MS 5000      // 每次增加5秒

int ota_retry_linear_backoff(int max_retries) {
    int delay = INITIAL_DELAY_MS;
    for (int i = 0; i < max_retries; i++) {
        int result = ota_download_chunk();
        if (result == SUCCESS) {
            return SUCCESS;
        }
        printf("第%d次失败,等待%dms\n", i+1, delay);
        delay_ms(delay);
        delay += STEP_DELAY_MS;
        if (delay > MAX_DELAY_MS) {
            delay = MAX_DELAY_MS;
        }
    }
    return FAILED;
}

线性退避的增长速度比指数退避慢,比固定间隔灵活。适合网络问题不会急剧恶化的场景。

4.5 四种策略对比

策略 等待时间公式 增长速度 适用场景 我个人的推荐度
固定间隔 常量 无增长 局域网、可控环境 ★★☆☆☆
指数退避 基数 × 2^n 外网、大规模并发 ★★★★★
随机退避 基础时间 + 随机抖动 可控 多设备同时重试 ★★★★☆
线性退避 初始值 + 步长 × n 中等 网络问题温和的场景 ★★★☆☆

4.6 实际项目中的选择建议

我个人习惯,在OTA升级中这样选:

  1. 首选指数退避 + 随机抖动。这是最稳妥的组合,能应对大部分网络异常。
  2. 固定间隔? 只在调试阶段用。上线前一定要换掉。
  3. 线性退避 适合那些「我知道问题大概多久能恢复」的场景。比如服务器定期维护,每次维护5分钟,那线性退避就很合适。

重要提醒: 不管用哪种策略,一定要设置最大重试次数和最大等待时间。我见过一个设备因为网络断了,按指数退避重试了整整三天,把电池耗光了。用户投诉说「你们的设备怎么一夜就没电了」——嗯,那场面挺尴尬的。

好了,这四种策略你心里有数了吧?下一节咱们聊聊更高级的话题——如何根据错误码动态调整重试策略。到时候我会分享一个我踩过的坑,跟HTTP 503状态码有关,挺有意思的。