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

刷新Token时暂停网络请求的优化方案咨询

解决Token刷新时重复触发网络请求的问题

问题描述

现有如下NetworkService类,功能正常但存在一个问题:在执行Token刷新请求期间,新到来的请求会重复触发Token刷新操作,造成不必要的网络调用。此前尝试过DispatchSemaphore但不适用于异步方法,还会导致请求串行;尝试用数组存储待处理请求但扩展复杂,寻求其他可行解决方案。

原代码

class NetworkService: NetworkRequestServicing {

private let tokenService: TokenServicing
private let httpClient: HTTPClient

weak var delegate: TokenDelegate?

public init(httpClient: HTTPClient, tokenService: TokenServicing) {
    self.httpClient = httpClient
    self.tokenService = tokenService
}

func sendAuthenticatedRequest<T: Decodable>(request: Request,
                                            responseModel: T.Type,
                                            isFormData: Bool,
                                            retryTimes: Int,
                                            additionalHeaders: [String: String]?) async -> Result<T, RequestError> {

    let result = await httpClient.sendRequest(request: request,
                                              responseModel: responseModel,
                                              retryTimes: retryTimes,
                                              additionalHeaders: additionalHeaders,
                                              isFormData: isFormData)
    switch result {
    case .success:
        return result
    case .failure(let error):
        if case RequestError.unauthorized = error {
            return await refresh(request: request, responseModel: responseModel)
        } else {
            return .failure(error)
        }
    }
}

private func refresh<T: Decodable>(request: Request,
                                   responseModel: T.Type,
                                   isFormData: Bool = false,
                                   sendBinaryRequest: ((Request) async -> Result<T, RequestError>)? = nil) async -> Result<T, RequestError> {
    print("New Refresh Token needed")

    guard let expiryDate = tokenService.getRefreshExpiryDate(),
          expiryDate > .now else {
        print("RefreshToken not valid")
        return await .failure(reLogin())
    }

    guard tokenService.getRefreshToken() != nil else {
        print("No refresh token available -> User must ReLogin")
        return await .failure(reLogin())
    }

    let numberOfRetries = 3
    for failedRefreshAttempts in 1...numberOfRetries {
        let result = await tokenService.tokenRefresh()

        switch result {
        case .success:
            print("Refresh Tokens succeeded")
            await sendRefreshToDelegates()
            if let sendBinaryRequest = sendBinaryRequest {
                return await sendBinaryRequest(request)
            } else {
                return await httpClient.sendRequest(request: request, responseModel: responseModel,
                                                    isFormData: isFormData)
            }
        case .failure(let error):
            print("\(failedRefreshAttempts). attempt of refreshing the tokens failed due to \(error)")
        }

         try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
    }

    return await .failure(reLogin())
}
}

解决方案:共享刷新任务

核心思路是维护一个全局唯一的刷新任务,当多个请求触发401需要刷新Token时,让所有请求等待同一个刷新任务完成,避免重复发起刷新请求。具体实现如下:

步骤1:添加共享任务管理属性

在NetworkService中添加两个私有属性,用于跟踪刷新任务并保证线程安全:

private var currentRefreshTask: Task<Result<Void, RequestError>>?
private let refreshLock = NSLock()

步骤2:重构刷新逻辑,新增协调方法

新增coordinateRefresh方法负责管理刷新任务的创建与共享;将原refresh方法拆分为仅执行刷新操作的performRefresh方法,分离职责。

修改后的完整代码

class NetworkService: NetworkRequestServicing {

private let tokenService: TokenServicing
private let httpClient: HTTPClient
// 新增:管理共享刷新任务与线程安全锁
private var currentRefreshTask: Task<Result<Void, RequestError>>?
private let refreshLock = NSLock()

weak var delegate: TokenDelegate?

public init(httpClient: HTTPClient, tokenService: TokenServicing) {
    self.httpClient = httpClient
    self.tokenService = tokenService
}

func sendAuthenticatedRequest<T: Decodable>(request: Request,
                                            responseModel: T.Type,
                                            isFormData: Bool,
                                            retryTimes: Int,
                                            additionalHeaders: [String: String]?) async -> Result<T, RequestError> {

    let result = await httpClient.sendRequest(request: request,
                                              responseModel: responseModel,
                                              retryTimes: retryTimes,
                                              additionalHeaders: additionalHeaders,
                                              isFormData: isFormData)
    switch result {
    case .success:
        return result
    case .failure(let error):
        if case RequestError.unauthorized = error {
            // 等待刷新任务完成后重试原请求
            let refreshResult = await coordinateRefresh()
            switch refreshResult {
            case .success:
                return await httpClient.sendRequest(request: request,
                                                   responseModel: responseModel,
                                                   retryTimes: retryTimes,
                                                   additionalHeaders: additionalHeaders,
                                                   isFormData: isFormData)
            case .failure(let refreshError):
                return .failure(refreshError)
            }
        } else {
            return .failure(error)
        }
    }
}

// 新增:协调刷新任务,保证同一时间仅一个刷新请求执行
private func coordinateRefresh() async -> Result<Void, RequestError> {
    refreshLock.lock()
    defer { refreshLock.unlock() }
    
    // 已有刷新任务时,直接等待其完成
    if let existingTask = currentRefreshTask {
        return await existingTask.value
    }
    
    // 创建新的刷新任务并记录引用
    let task = Task {
        let result = await performRefresh()
        // 任务完成后清理引用
        refreshLock.lock()
        currentRefreshTask = nil
        refreshLock.unlock()
        return result
    }
    currentRefreshTask = task
    return await task.value
}

// 拆分后仅负责执行Token刷新逻辑
private func performRefresh() async -> Result<Void, RequestError> {
    print("New Refresh Token needed")

    guard let expiryDate = tokenService.getRefreshExpiryDate(),
          expiryDate > .now else {
        print("RefreshToken not valid")
        return .failure(reLogin())
    }

    guard tokenService.getRefreshToken() != nil else {
        print("No refresh token available -> User must ReLogin")
        return .failure(reLogin())
    }

    let numberOfRetries = 3
    for failedRefreshAttempts in 1...numberOfRetries {
        let result = await tokenService.tokenRefresh()

        switch result {
        case .success:
            print("Refresh Tokens succeeded")
            await sendRefreshToDelegates()
            return .success(())
        case .failure(let error):
            print("\(failedRefreshAttempts). attempt of refreshing the tokens failed due to \(error)")
        }

         try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
    }

    return .failure(reLogin())
}

// 原reLogin方法(假设已实现)
private func reLogin() -> RequestError {
    return RequestError.unauthorized
}
}

关键逻辑说明

  1. 共享任务管理:coordinateRefresh会先检查是否已有正在执行的刷新任务,存在则直接等待;不存在则创建新任务,后续请求复用同一个任务,彻底避免重复刷新。
  2. 线程安全:用NSLock保证对currentRefreshTask的读写操作线程安全,避免多线程环境下的竞态问题。
  3. 异步非阻塞:等待刷新任务的过程是异步的,不会阻塞其他正常请求,仅触发401的请求会等待刷新完成后重试。
  4. 职责分离:刷新逻辑与任务协调逻辑拆分,代码结构更清晰,便于后续维护扩展。

内容的提问来源于stack exchange,提问作者Mohamed Emad Hegab

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 20:59:55