使用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
相关产品推荐
相关产品推荐

