Retry Policy — 重试策略 — 定义移动应用何时以及如何自动重试失败网络调用的一套规则。在不稳定的连接或临时服务器错误的情况下,良好的重试策略无需用户参与即可提高应用的可靠性。根据Google Developer Relations(2025)的研究,正确实施Retry Policy可将频繁进行网络操作的移动应用中丢失请求的比例降低40-60%。
要点
Retry Policy — 是一种软件策略,定义客户端在网络请求失败时的行为:哪些错误应重试、重试多少次、以什么延迟以及何时停止尝试。在移动应用中,由于移动网络的不稳定性和服务器端可能的临时故障,重试策略至关重要。
基本的Retry Policy包含三个参数:最大重试次数(maxRetries)、初始延迟(baseDelay)和延迟增加策略(backoff strategy)。此外,可以指定需要以重试响应的HTTP状态码列表,以及中断所有尝试的超时时间。
根据Martin Kleppmann的“Designing Data-Intensive Applications”一书,分布式系统中50%的故障是暂时的,可以通过重试解决。这使得Retry Policy成为在不改变服务器架构的情况下提高移动应用容错性的最有效且最廉价的方法之一。
临时错误(retriable)—— Retry Policy应响应的唯一故障类型。这些包括连接超时(SocketTimeoutException)、服务器临时不可用(HTTP 503、502)和DNS错误。永久错误——HTTP 400、401、403、404——重试没有意义,因为它们表明请求本身有问题,而非网络或服务器。
根据AWS Architecture Blog的研究,将错误正确分类为retriable和non-retriable是设计Retry Policy时最重要的决定。重复非幂等的HTTP 401请求可能导致帐户被锁定,而重复HTTP 400可能导致数据重复。始终显式配置要重试的代码列表。
Fixed interval — 最简单的策略:每次重试在相同时间间隔后执行。例如,延迟2秒时,应用在2、2、2秒后重试请求。Fixed interval实现简单且可预测,但在大规模故障时对服务器产生均匀负载。
Incremental interval — 延迟随每次重试线性增加:第一次重试在1秒后,第二次在2秒后,第三次在3秒后,依此类推。这种策略在重复故障时为服务器提供更多恢复时间,但对于同时失败的大量客户端仍然可预测。
| 策略 | 延迟公式 | 累计时间(3次尝试) | 应用 |
|---|---|---|---|
| Fixed | delay = D | 3 × D | 简单场景,本地超时 |
| Incremental | delay = N × D | 6 × D | 逐步降低负载 |
| Exponential | delay = D × 2^N | 7 × D | 大规模故障,云服务 |
| Exponential + Jitter | delay = random(0, D × 2^N) | 可变 | 高负载,微服务 |
策略的选择取决于应用的性质。对于移动设备上数据同步的后台任务,带jitter的指数策略是最优的——以最小的服务器和用户设备负载提供最高的成功概率。
Exponential backoff — 每次尝试之间延迟加倍的策略。如果初始延迟为1秒,则延迟序列为1、2、4、8、16秒。这为服务器提供了指数级增长的恢复时间。
Jitter — 防止多个客户端同步重复请求的随机延迟偏差(thundering herd问题)。没有jitter,一千个具有相同Retry Policy的客户端将同时重复请求,在服务器上产生峰值负载。Jitter在时间上分散重试。
Kotlin协程可以在不阻塞主线程的情况下实现带jitter的exponential backoff。kotlinx-coroutines中的retry函数接受重试条件和包含请求体的代码块,自动管理延迟和尝试次数。
suspend fun RetryPolicy.executeWithRetry(
block: suspend () -> Result<T>
): Result<T> {
var lastError: Throwable? = null
repeat(maxRetries + 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (!isRetriable(e) || attempt == maxRetries) {
return Result.failure(e)
}
val delay = (baseDelayMs * (1 shl attempt))
.toLong()
val jitteredDelay = (delay * (0.5 + Random.nextDouble())).toLong()
delay(jitteredDelay)
lastError = e
}
}
return Result.failure(lastError!!)
}
executeWithRetry函数接受一个包含网络调用的lambda,并以exponential backoff和jitter执行它。如果错误不可重试(non-retriable)或超过最大尝试次数,函数返回错误。延迟乘以0.5到1.5之间的随机系数,以实现重试的均匀分布。
Circuit Breaker — 一种设计模式,在服务长时间不可用时防止无限重复请求。当错误数量超过阈值时,Circuit Breaker进入OPEN状态并立即返回错误而不执行请求,让服务器有时间恢复。
在移动应用中,当API因计划维护或运营商网络故障而不可用时,Circuit Breaker特别有用。没有它,应用将把电池和流量浪费在无限重试上,恶化用户体验并缩短设备的电池寿命。
三种状态Circuit Breaker:CLOSED(正常运行,请求被执行)、OPEN(拒绝,请求被阻止)和HALF_OPEN(用于检查恢复的测试请求)。在OPEN状态下经过指定的超时时间后,开关进入HALF_OPEN并执行一个请求——成功时返回CLOSED,失败时进入OPEN。
class CircuitBreaker(
private val failureThreshold: Int = 3,
private val timeoutMs: Long = 30000
) {
private var state = State.CLOSED
private var failureCount = 0
private var lastFailureTime: Long = 0
suspend fun T.protect(block: suspend () -> T): T {
checkState()
return try {
val result = block()
onSuccess()
result
} catch (e: Exception) {
onFailure()
throw e
}
}
}
Kotlin中的Circuit Breaker实现包含错误计数器和恢复定时器。protect方法在执行被阻止的请求之前检查当前状态,并在故障时更新错误计数器。达到failureThreshold阈值后,所有请求立即被拒绝,直到timeoutMs过期。
移动网络具有使Retry Policy特别重要的特性。Wi-Fi和移动数据之间的切换、地铁和隧道中的信号丢失、运营商级别的临时封锁——所有这些场景都会导致请求失败,而重试可以成功处理这些失败。
在Android上,Retrofit库和OkHttp通过Interceptor提供内置的RetryPolicy机制。在iOS上,问题通过URLSessionConfiguration和自定义委托解决。对于跨平台开发,Ktor(KMP)包含内置的retry支持,具有可配置的策略。
Combine — Apple的响应式编程框架。Combine中的retry操作符在出错时重复发布者指定次数,但不允许配置重试之间的延迟。对于完整的Retry Policy,使用catch和flatMap带延迟的自定义组合。
extension Publisher {
func retryWithBackoff(
retries: Int = 3,
baseDelay: TimeInterval = 1.0
) -> AnyPublisher<Output, Failure> {
return self.catch { error -> AnyPublisher in
guard retries > 0 else {
return Fail(error).eraseToAnyPublisher()
}
return Just(())
.delay(for: .seconds(baseDelay), scheduler: DispatchQueue.main)
.flatMap { self.retryWithBackoff(
retries: retries - 1,
baseDelay: baseDelay * 2
) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
Combine中Publisher的retryWithBackoff扩展通过递归调用(减少计数器和加倍延迟)实现exponential backoff。delay操作符在重试之间创建暂停,catch捕获错误并决定是重试还是返回failure。
第一个错误——在不检查幂等性的情况下重试请求。如果服务器已创建资源但因网络故障未返回确认,重复请求将创建重复数据。对于POST请求,始终在标头中使用幂等键(Idempotency-Key),或仅对GET、PUT和DELETE应用retry。
第二个错误——无限重试(retry forever)。始终设置最大尝试次数(移动应用为3-5次)和所有尝试的总超时时间。无限重试会消耗电池并在服务器上产生寄生负载,特别是在数据库迁移或API更改时。
第三个错误——忽略应用的上下文。如果用户关闭了应用或进入后台模式,活动的Retry Policy应正确取消。使用带有SupervisorScope的协程或具有UI生命周期的Combine,在屏幕关闭时自动取消重试。
第四个错误——不记录重试尝试。没有记录,您将不知道有多少请求被重试、出现了哪些错误以及您的Retry Policy有多有效。添加指标:重试次数、重试后的成功率、延迟分布。这些数据将有助于为特定应用调整最优策略参数。
常见问题
大多数场景的最佳重试次数为3-5次。后台同步允许5-7次,交互式请求(例如提交表单)——不超过3次。更多重试不会增加成功概率,但会消耗用户的电池和流量。
Exponential backoff——重试尝试之间延迟加倍:1秒、2、4、8、16,依此类推。如果服务器过载,第一次重试之间的短暂暂停使其能够快速响应,而每次后续尝试中逐渐增加的暂停则为服务器提供越来越多的恢复时间。
只重试临时错误:408(Request Timeout)、429(Too Many Requests)、502(Bad Gateway)、503(Service Unavailable)、504(Gateway Timeout)。4xx错误(408和429除外)表示客户端问题——重试它们没有意义,并且可能对用户数据造成危险。
Retry Policy管理单个请求在失败时的重试。Circuit Breaker管理与服务的连接状态:当错误累积时,它断开电路(OPEN)并阻止新请求。Retry在单个调用级别工作,Circuit Breaker在服务集成级别工作。
要测试Retry Policy,在Android上使用NetworkInterceptor(OkHttp),在iOS上使用URLProtocol(URLSession)模拟网络故障。设置参数:错误频率、不可用持续时间和响应代码。使用MockWebServer(OkHttp)或OHHTTPStubs(iOS)的单元测试在没有真实网络的情况下检查重试逻辑。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。