11-18.【错误处理】网络请求或数据库操作中,如何设计错误恢复和重试策略?(如指数退避)

3 阅读3分钟

在网络请求或数据库操作中,设计错误恢复策略的核心在于区分“可重试”与“不可重试”错误,并防止重试行为导致系统级雪崩。

一个成熟的防御式重试策略通常包含:识别、退避、抖动和熔断


1. 识别:哪些错误值得重试?

不是所有失败都应该重试。

  • 可重试错误

    • 网络波动:如超时(Timeout)、DNS 解析失败。
    • 服务端繁忙:HTTP 429 Too Many Requests503 Service Unavailable
    • 数据库死锁/连接超时:如 SQLite_BUSY 或临时连接丢失。
  • 不可重试错误

    • 逻辑错误404 Not Found401 Unauthorized(需重新登录)、400 Bad Request
    • 数据损坏:数据库架构不匹配、违反唯一性约束。

2. 指数退避(Exponential Backoff)

如果服务器因过载而失败,立即重试只会加剧负担。指数退避通过增加每次尝试之间的等待时间来缓解压力。

公式delay=base×2ndelay = base \times 2^{n}nn 为当前重试次数)。

结合“抖动”(Jitter)

如果大量客户端在同一时刻失败并使用相同的退避逻辑,它们会在同一时间点再次发起请求(惊群效应)。通过引入随机波动(Jitter) ,可以将请求均匀分散。

防御式公式delay=random(0,base×2n)delay = \text{random}(0, base \times 2^{n})


3. 在 Swift 中的工程实现

利用 Swift 的 async/await,我们可以优雅地实现一个通用的重试包装器:

Swift

func retry<T>(
    maxAttempts: Int = 3,
    baseDelay: TimeInterval = 1.0,
    task: () async throws -> T
) async throws -> T {
    for attempt in 0..<maxAttempts {
        do {
            return try await task()
        } catch {
            // 检查是否达到最大尝试次数,或是否为不可重试错误
            if attempt == maxAttempts - 1 || !isRetryable(error) {
                throw error
            }
            
            // 指数退避 + 抖动计算
            let delay = pow(2.0, Double(attempt)) * baseDelay
            let jitter = Double.random(in: 0...(0.1 * delay))
            let finalDelay = delay + jitter
            
            Logger.log("第 (attempt + 1) 次失败,(finalDelay)s 后重试...")
            try await Task.sleep(nanoseconds: UInt64(finalDelay * 1_000_000_000))
        }
    }
    // 理论上不会执行到这里,由 throw 终止
    throw URLError(.unknown)
}

4. 数据库特有的恢复策略:WAL 与事务回滚

数据库重试与网络请求略有不同,重点在于原子性

  • 事务重试:如果数据库返回 busy 状态,通常意味着另一个写操作正在锁定。此时应重试整个事务块,而不仅仅是最后一条 SQL。
  • WAL 模式(Write-Ahead Logging) :开启 WAL 可以允许并发读写,极大减少“数据库忙”报错的概率。
  • 防御式自愈:如果检测到数据库文件损坏,重试无效。应设计自动触发逻辑,尝试从最近的备份中恢复,或者删除损坏文件并重新同步数据。

5. 高级模式:熔断器(Circuit Breaker)

当某个服务持续报错(如重试 10 次都失败)时,应暂时“断开”该服务的访问。

  • 状态

    • Closed(关闭) :正常工作。
    • Open(开启) :服务故障,直接返回错误,不再尝试请求。
    • Half-Open(半开) :放行少量请求,若成功则恢复正常,否则重新断开。

总结:重试设计清单

  1. 限制最大次数:通常 3-5 次。
  2. 设置总超时:即使单次请求没超时,整个重试链路也应有截止时间(Deadline)。
  3. 识别幂等性非常重要! 只有幂等请求(如 GET、PUT 更新、DELETE)可以安全重试。非幂等请求(如 POST 支付订单)在未确认服务器接收状态前禁止自动重试。
  4. 感知取消:重试期间必须检查 Task.isCancelled,防止用户退出界面后请求仍在后台无谓重试。