You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 19:10:33