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

使用Retrofit/OkHttp Authenticator时如何避免并行刷新Token请求?

解决并行Refresh Token请求的竞态问题

你遇到的这个问题很典型:多个请求同时触发token刷新时,全都调用refresh token接口,不仅浪费API资源,还会生成不同的refresh token导致状态不一致。你用StateFlow+Channel的思路方向是对的,但实现确实可以更简洁,下面给你几个优化方案:

原方案的问题点

先说说你现有代码里的几个潜在问题:

  • 无作用域的CoroutineScope(Dispatchers.IO).launch没绑定生命周期,可能造成协程泄漏
  • isRefreshingToken.collect会持续监听流,刷新完成后也不会自动取消,浪费资源
  • 用Channel传递结果有点冗余,完全可以用更轻量的同步机制替代

优化方案1:用Mutex实现互斥刷新

Kotlin协程的Mutex是专门用来处理互斥访问的工具,能确保同一时间只有一个请求执行刷新逻辑,其他请求等待锁释放后直接复用最新token,代码简洁很多:

class MyAuthenticator @Inject constructor(
    private val refreshTokenUseCase: RefreshTokenUseCase,
    private val sharedPrefs: SharedPreferences
) : Authenticator {

    // 控制刷新操作的互斥锁
    private val refreshMutex = Mutex()

    override fun authenticate(route: Route?, response: Response): Request? {
        return runBlocking(Dispatchers.IO) {
            refreshMutex.withLock {
                val currentToken = sharedPrefs.getToken().orEmpty()
                // 判断是否需要刷新token(根据响应状态码和当前token有效性)
                val needRefresh = response.code == 401 && currentToken.isNotEmpty()

                if (!needRefresh) {
                    // token还能用,直接返回带当前token的请求
                    return@runBlocking response.request.newBuilder()
                        .header("Authorization", "Bearer $currentToken")
                        .build()
                }

                // 只有第一个拿到锁的请求会执行刷新
                val result = refreshTokenUseCase()
                return@runBlocking if (result.isSuccess) {
                    val newToken = sharedPrefs.getToken().orEmpty()
                    response.request.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                } else {
                    // 刷新失败,处理退出登录等逻辑
                    null
                }
            }
        }
    }
}

这个方案的优势

  • 代码简洁,去掉了冗余的Flow和Channel操作
  • withLock确保同一时间只有一个请求执行刷新,彻底杜绝重复调用API的问题
  • 无需手动管理协程作用域,避免协程泄漏风险

优化方案2:用Deferred缓存刷新结果

如果你的刷新请求触发频率很高,可以用Deferred缓存当前的刷新任务,让后续请求直接等待这个任务的结果,进一步减少锁的竞争:

class MyAuthenticator @Inject constructor(
    private val refreshTokenUseCase: RefreshTokenUseCase,
    private val sharedPrefs: SharedPreferences
) : Authenticator {

    // 缓存当前的刷新任务,避免重复发起请求
    @Volatile
    private var refreshJob: Deferred<Result<Unit>>? = null

    override fun authenticate(route: Route?, response: Response): Request? {
        return runBlocking(Dispatchers.IO) {
            val currentToken = sharedPrefs.getToken().orEmpty()
            val needRefresh = response.code == 401 && currentToken.isNotEmpty()

            if (!needRefresh) {
                return@runBlocking response.request.newBuilder()
                    .header("Authorization", "Bearer $currentToken")
                    .build()
            }

            // 要么复用正在进行的刷新任务,要么发起新的
            val job = refreshJob ?: refreshTokenUseCase().also { refreshJob = it }
            val result = job.await()
            
            // 刷新完成后清空缓存,下次刷新重新发起
            refreshJob = null

            return@runBlocking if (result.isSuccess) {
                val newToken = sharedPrefs.getToken().orEmpty()
                response.request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
            } else {
                null
            }
        }
    }
}

额外建议

如果你的OkHttp版本支持,可以考虑使用SuspendAuthenticator(部分OkHttp扩展库提供),这样就能去掉runBlocking,让代码更贴合协程的最佳实践。

内容的提问来源于stack exchange,提问作者BP9381

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 16:48:26