基于Retrofit、协程的并行请求401重试及Token刷新方案问询
Retrofit+协程并行请求下401/422的Token刷新与重试方案分析
一、现有拦截器实现的核心问题
常规的ServiceInterceptor在并行请求场景下存在致命缺陷:
- 多个请求同时触发401/422时,会同时调用refresh token接口,不仅浪费资源,还可能导致后端返回冲突错误,新token无法正常更新
- 拦截器的
intercept方法不是挂起函数,硬套协程逻辑要么阻塞线程,要么出现token更新的竞态问题——比如A请求刚刷新完新token,B请求拿着旧token又发起刷新,直接覆盖掉新token
二、并行场景下的正确处理姿势
1. 核心思路:加锁控制Token刷新并发
必须用全局唯一的协程安全锁,保证同一时间只有一个请求执行refresh操作,其他触发401的请求等待锁释放后,直接用新token重试,避免重复刷新。
2. 具体实现代码
第一步:封装线程安全的Token管理类
class TokenManager { private var accessToken: String? = null private var refreshToken: String? = null // 用Mutex实现协程安全的锁 private val refreshLock = Mutex() // 协程安全的获取当前token suspend fun getCurrentToken(): String? = refreshLock.withLock { accessToken } // 同一时间仅一个请求能执行刷新逻辑 suspend fun refresh(): Result<String> = refreshLock.withLock { // 调用你的refresh token接口 val refreshResponse = AuthApi.instance.refreshToken(refreshToken!!) return if (refreshResponse.isSuccessful) { val newToken = refreshResponse.body()!! accessToken = newToken.accessToken refreshToken = newToken.refreshToken Result.success(newToken.accessToken) } else { Result.failure(RefreshTokenFailedException()) } } }
第二步:适配协程的Auth拦截器
由于OkHttp的Interceptor不是挂起函数,这里用runBlocking切换到IO线程处理刷新逻辑(如果用Retrofit的协程CallAdapter,也可以把拦截逻辑放到请求的协程上下文里,实现更优雅):
class AuthInterceptor( private val tokenManager: TokenManager ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() // 给请求添加token头 val authenticatedRequest = originalRequest.newBuilder() .addHeader("Authorization", "Bearer ${tokenManager.getCurrentToken()}") .build() val initialResponse = chain.proceed(authenticatedRequest) // 处理401/422状态码 if (initialResponse.code == 401 || initialResponse.code == 422) { // 先关闭原响应,避免资源泄漏 initialResponse.close() return runBlocking(Dispatchers.IO) { return@runBlocking when (val refreshResult = tokenManager.refresh()) { is Result.Success -> { // 用新token重试原请求 val newRequest = authenticatedRequest.newBuilder() .header("Authorization", "Bearer ${refreshResult.data}") .build() chain.proceed(newRequest) } is Result.Failure -> { // 刷新失败,返回原错误(或直接抛异常触发登录跳转) initialResponse } } } } return initialResponse } }
第三步:并行请求的发起
用lifecycleScope发起并行请求时,无需额外修改请求逻辑,只要TokenManager是全局单例,拦截器会自动处理并发刷新:
lifecycleScope.launch { val request1 = async { ApiService.instance.getList1() } val request2 = async { ApiService.instance.getList2() } val request3 = async { ApiService.instance.getList3() } try { val result1 = request1.await() val result2 = request2.await() val result3 = request3.await() // 处理请求结果 } catch (e: Exception) { // 统一处理错误(比如刷新失败时跳转登录页) } }
三、必避的坑
- 锁必须全局唯一:
TokenManager一定要做成单例,否则每个拦截器实例带独立锁,还是会出现重复刷新 - 不要阻塞主线程:刷新token的逻辑必须放到IO线程,
runBlocking里指定Dispatchers.IO,避免卡死UI - 加重试次数限制:给刷新操作加次数限制(比如最多1次),防止后端持续返回401时无限循环
- 刷新失败统一处理:如果刷新token返回401,说明refresh token也过期,直接跳转登录页,避免所有请求挂起
内容的提问来源于stack exchange,提问作者Sonu Kumar
相关产品推荐
相关产品推荐

