如何在协程中优雅地处理网络请求失败并进行重试?
解读
面试官真正想考察的是:
- 你对 Kotlin 协程结构化并发与异常传播链的理解深度;
- 能否把“重试”做成可复用、可测试、不污染业务代码的通用组件;
- 对国内网络环境(弱网、运营商劫持、IPv6 双栈、网关 502/504 高发)是否有落地经验;
- 是否知道在 Android 生命周期内安全地取消重试,避免内存泄漏或 Crash;
- 对 Retrofit+OkHttp 现有重试机制与协程侧重试的边界分工是否清晰。
一句话:不是“写个循环”就行,而是让重试“像空气一样存在,又像水一样透明”。
知识点
- 协程异常传播:launch 用 CoroutineExceptionHandler,async 在 await() 时抛;结构化并发保证子协程异常向上传递。
- 重试策略:立即重试、固定间隔、指数退避(Exponential Backoff)、带抖动(Jitter)避免“雷群效应”。
- 取消敏感性:重试内部必须检查 isActive,每次 delay 前使用 ensureActive(),让外部 Job.cancel() 能秒级响应。
- 国内特色错误码:HTTP 522/524(CDN 超时)、运营商 302 广告、未知证书(-998)需单独识别,避免无脑重试。
- 分层职责:OkHttp 的 interceptor 做“快速重试”(3 次、毫秒级)、协程层做“业务重试”(3~5 次、秒级),两者互补。
- 日志与埋点:每次重试需记录 traceId、耗时、错误类型,方便后端按用户维度排查。
- 测试:使用 MockWebServer + KotlinTest 的“协程测试”扩展,验证重试次数、间隔、取消点。
答案
给面试官一个“可直接拷贝到生产模块”的模板,分三步讲:
第一步:定义“可配置”的重试策略
data class RetryConfig(
val maxAttempts: Int = 3,
val initialDelayMs: Long = 500,
val maxDelayMs: Long = 10_000,
val factor: Double = 2.0,
val jitter: Boolean = true
)
第二步:写一个纯函数式重试器,不依赖 Android 框架,方便单元测试
suspend fun <T> retryWithPolicy(
config: RetryConfig = RetryConfig(),
block: suspend (attempt: Int) -> T
): T {
var currentDelay = config.initialDelayMs
repeat(config.maxAttempts) { attempt ->
try {
return block(attempt + 1)
} catch (e: Exception) {
// 国内常见不可重试异常,直接抛
when {
e is CancellationException -> throw e
e is SSLHandshakeException -> throw e
e is UnknownHostException && attempt == 0 -> throw e
}
if (attempt == config.maxAttempts - 1) throw e
// 指数退避 + 抖动
if (config.jitter) {
currentDelay = (currentDelay * config.factor * (0.5 + Random.nextDouble(0.5))).toLong()
} else {
currentDelay = (currentDelay * config.factor).toLong()
}
currentDelay = currentDelay.coerceAtMost(config.maxDelayMs)
delay(currentDelay)
}
}
error("Unreachable")
}
第三步:在 ViewModel 里与协程作用域绑定,支持自动取消
class HomeViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
val uiState: StateFlow<UiState> = _uiState
fun loadHomePage() = viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching {
retryWithPolicy {
// 直接抛异常即可触发重试
apiService.getHome()
}
}.onSuccess {
_uiState.value = UiState.Success(it)
}.onFailure {
_uiState.value = UiState.Error(it.mapToMessage())
}
}
}
亮点补充(面试时主动说):
- 用 ensureActive() 保证用户退出页面时重链立刻中断,不会浪费流量;
- 对 401 令牌失效特殊处理:第一次直接 refreshToken,第二次再失败才抛到外层;
- 与埋点平台对齐,每次重试带 x-request-id,方便后端按链路追踪;
- 单元测试用 Turbine 测试 StateFlow,重试 3 次耗时 3.5 s,误差 < 100 ms。
拓展思考
- 如果重试策略需要动态下发(运营配置),可以把 RetryConfig 做成 Firebase RemoteConfig 或国内“友盟+”配置通道,热更新不重启进程。
- 对上传场景(如图片)应改用“分段重试”+“断点续传”,协程层只负责调度,真正的断点逻辑放到 OkHttp 的 RequestBody 里,避免重复压缩。
- 车载与 Wear 场景下网络切换频繁,可监听 ConnectivityManager.NetworkCallback,在网络恢复后自动重置重试计数器,而不是盲目继续 delay。
- 高并发接口(如秒杀)需引入“熔断”与“重试”分离:熔断走快速失败,重试只用在读多写少场景,防止放大流量把后端打挂。
- Kotlin 1.9 后支持“自动限频”协程调度器,可在重试器内部集成 Semaphore,控制全局重试 QPS,避免进程级“重试风暴”。