Shaula Blog

美丽的东西都是肤浅的

iOS 智能重试机制

网络请求、API 调用这种异步操作,失败其实是家常便饭。怎么处理失败、要不要自动重试、怎么重试才能既解决问题又不把服务器搞崩——这几乎是每个 iOS 开发者都会遇到的现实问题。

这篇文章分享一个我自己在用的 Swift 重试实现,核心是指数退避加一点随机抖动,用起来还算顺手。

它长什么样

指数退避:别急着再来一次

每次重试的等待时间按指数往上翻:delay × 2^attempt。这样能避免一窝蜂地反复请求,给服务端留口气。

实际效果大概是:

第 1 次重试:等 1.0 秒
第 2 次重试:等 2.0 秒
第 3 次重试:等 4.0 秒
第 4 次重试:等 8.0 秒

加个 Jitter,避免惊群

多个客户端同时失败、同时重试,很容易造成“惊群效应”——大家都挤在同一时间点打过来。解决办法是给等待时间加一点随机偏移。

// jitter=0.5 时,理论退避 4 秒,实际会落在 2 到 6 秒之间
let jitterAmount = waitTime * jitter
waitTime += Double.random(in: -jitterAmount...jitterAmount)

不是所有错误都值得重试

有些错误重试也没用,比如 404、权限错误。所以留了一个 shouldRetry 闭包,让你自己决定哪些错误才需要重试:

shouldRetry: { error in
    if let urlError = error as? URLError {
        return [.timedOut, .notConnectedToInternet].contains(urlError.code)
    }
    return false
}

怎么实现的

核心代码其实不复杂,就是一个 for 循环加上异步等待:

for attempt in 0...maxRetries {
    do {
        return try await operation()
    } catch {
        lastError = error
        if attempt < maxRetries && shouldRetry(error) {
            let waitTime = calculateWaitTime(attempt)
            try await Task.sleep(for: .seconds(waitTime))
        }
    }
}

几个关键点:

  • 把最后一次错误存下来,最终要抛出去
  • 重试之前检查两个条件:没到最大次数,并且错误类型允许重试
  • Task.sleep 是非阻塞的,不会卡线程

什么时候用得上

网络请求重试

let data = try await withSmartRetry(
    maxRetries: 5,
    delay: 2.0,
    jitter: 0.3
) {
    try await apiClient.fetchUserData()
}

模型推理服务

let result = try await withSmartRetry(
    shouldRetry: { error in
        guard let httpError = error as? HTTPError else { return false }
        return (500...599).contains(httpError.statusCode)
    }
) {
    try await modelService.predict(input: "hello")
}

配置同步

let config = try await withSmartRetry(
    maxRetries: 3,
    delay: 1.0
) {
    try await configService.syncRemoteConfig()
}

几点小建议

  1. 重试次数别贪多:一般 3 次够用,关键业务可以适当加一点
  2. 分布式环境记得开 Jitter:不然大家一起重试,等于没重试
  3. 只对可恢复的错误重试:有些错误重试一百遍也没用
  4. 留个心眼记一下日志:重试了多少次、成功没有,后面调参数用得着

注意别踩坑

  • 重试意味着总耗时变长,别忘了配合超时机制
  • 只有幂等操作适合重试。比如“扣款”这种操作,重试前要想清楚
  • 任务被取消时,Task.sleep 会抛出 CancellationError,重试会自然终止,不用额外处理

总结

智能重试其实就是两件事:退避算法避免压垮服务,抖动策略分散请求高峰。把这套东西封装好,你的异步操作会结实很多,不至于一遇到网络波动就直接崩了。