如何使用 OkHttp 实现请求重试、超时控制和连接池复用?
解读
国内面试中,OkHttp 几乎是“网络必考题”。面试官想确认三件事:
- 你是否只会“调库”,还是能根据业务场景把参数配到“刚刚好”;
- 遇到弱网、CDN 抖动、运营商劫持等中国特色场景,能否用重试/超时策略兜底;
- 高并发业务(如电商秒杀、直播礼物)下,连接池配错直接导致端口耗尽或 SYN 风暴,你有没有踩过坑。
因此,回答时要“落地”:给出代码模板、参数含义、踩坑案例,以及监控手段。
知识点
- 超时四兄弟:callTimeout、connectTimeout、readTimeout、writeTimeout;
- 重试两层次:OkHttp 内置重试(RetryOnConnectionFailure)与业务重试(Interceptor);
- 连接池:ConnectionPool 内部用 ConcurrentLinkedQueue + 线程清理器,keep-alive 时间默认 5 min,最大空闲连接数默认 5;
- 国内特色:
– 域名被运营商 TTL 缓存,DNS 轮询失效,需结合 HttpDns;
– 弱网下 TCP 握手 3 s 才失败,需把 connectTimeout 降到 2 s 以内,快速换 IP;
– 直播场景上传日志,writeTimeout 必须大于“最大分片时间”,否则 408 频发; - 监控:EventListener 统计重试次数、连接复用率、TCP 握手耗时,接入阿里 SLS 或腾讯 RUM。
答案
“我在上一个电商项目里把网络模块拆成独立组件,核心思路是‘三件套’:超时配准、重试分层、连接池可观测。”
- 超时控制
val client = OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS) // 整次调用硬上限,防止“伪死”
.connectTimeout(2, TimeUnit.SECONDS) // 国内 4G/5G 切换场景,2 s 足够
.readTimeout(8, TimeUnit.SECONDS) // 下载商品大图,8 s 兜底
.writeTimeout(5, TimeUnit.SECONDS) // 上传埋点,5 s 容错
.build()
注意:callTimeout 会覆盖单个请求的总耗时,出现“慢接口”时需后台配合拆分。
- 请求重试
内置开关默认开启,但只重试“幂等”请求(GET、HEAD)。对 POST/PUT 支付接口,必须自定义 Interceptor 做“业务重试”:
class BusinessRetryInterceptor(
private val maxRetry: Int = 2,
private val retryCodes: List<Int> = listOf(408, 502, 503, 504)
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var response: Response? = null
var exception: IOException? = null
for (i in 0..maxRetry) {
try {
response = chain.proceed(chain.request())
if (!response.isSuccessful && response.code in retryCodes) {
response.close()
if (i < maxRetry) continue
}
return response
} catch (e: IOException) {
exception = e
if (i < maxRetry) continue else throw exception
}
}
throw exception!!
}
}
接入时放在普通拦截器之后、网络拦截器之前,避免重复添加公共头。
- 连接池复用
val pool = ConnectionPool(8, 3, TimeUnit.MINUTES)
val client = OkHttpClient.Builder()
.connectionPool(pool)
.build()
参数解释:
– 8:秒杀高峰并发接口 200 QPS,经验值 8 条连接可把 3 次握手耗时从 120 ms 降到 20 ms;
– 3 min:短于运营商 NAT 超时(一般 5 min),防止 FIN_WAIT2 堆积。
配合“同一 host 复用”策略,所有图片域名收敛到 cdn.xxx.com,减少 TLS 握手次数 30%+。
- 可观测
自定义 EventListener,把“connectionAcquired”“callFailed”事件打到美团 Logan 日志,灰度期间发现重试率 >5% 的接口,立即回退后台或扩容 CDN。
这样配置后,线上网络请求失败率从 1.2% 降到 0.3%,GC 抖动减少 15%,用户支付取消率下降 0.8 个百分点。
拓展思考
- HTTP/2 多路复用 vs. 连接池:如果服务端已开启 HTTP/2,理论上一条 TCP 连接即可多路并发,但国内部分高防 CDN 会强制降级到 HTTP/1.1,此时仍依赖连接池。
- 预连接(preconnect):利用
client.newCall(Request.Builder().url("https://cdn.xxx.com").build()).enqueue(object : Callback{ override fun onResponse(call, response) { response.close() } override fun onFailure(call, e) {} })提前握手,把耗时隐藏在首屏渲染前。 - 与 Retrofit 结合:Retrofit 默认使用全局 OkHttpClient,但不同模块需要不同超时策略时,可用
@OkHttpClientWithTimeout自定义注解,配合 Retrofit 的callFactory注入,实现“一个接口一套策略”。 - 弱网模拟:国内主流做法是接入腾讯 QNET 或阿里 ATC,在地铁、电梯场景做 2G/3G 抖动压测,验证重试次数与超时阈值是否合理。