背景
在网络数据交互中,为了识别用户的身份,一般会设置Token
来处理与用户相关的业务逻辑;然而为了确保用户的信息安全,或是单点登录,那么在使用Token
的时候会设置有效时间;当Token
时间过期时,业务接口会抛出指定的错误码,基于该错误码来进行Token
刷新及相关逻辑。
当多个接口同时需要刷新Token
时,容易造成Token
重复刷新,或Token边刷新边失效的情况,鉴于此种情况,特介绍如下解决方案;
分析
首先分析可能引发这一问题出现的具体情形,比如 R1
,R2
,R3
三个请求都是需要Token
的接口,而此时Token
已失效,那么请求这三个接口必然会失败进入Token
刷新逻辑,需要处理的情形大致分为如下两种:
- 当
R1
正在刷新Token
中,而R2
和R3
准备发起请求; - 当
R1
准备刷新Token
时,而R2
和R3
也准备刷新Token
;
准备工作
模拟判断Token
是否失效:
var tokenable: Bool {
get {
UserDefaults.standard.bool(forKey: "token")
}
set {
UserDefaults.standard.set(newValue, forKey: "token")
}
}
模拟请求Token的刷新:
func _request() -> Single<Bool> {
return Single.create { [weak self] observer in
let item = DispatchWorkItem {
print("Token刷新完毕")
tokenable = true
observer(.success(true))
}
print("正在刷新Token...")
self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)
return Disposables.create {
item.cancel()
}
}
}
模拟请求业务接口:
func _request(url: String) -> Single<String> {
return Single.create { [weak self] observer in
let item = DispatchWorkItem {
if tokenable {
print("数据请求完毕:\(url)")
observer(.success(url))
} else {
print("Token过期:\(url)")
observer(.success(""))
}
}
print("正在请求数据:\(url)")
self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)
return Disposables.create {
item.cancel()
}
}
}
解决方案
基于第一种情况的解决办法,标记Token
的刷新状态,当R1
正在刷新时,R2
和R3
的请求先挂起,直到R1
的Token
刷新完成后再继续R2
和R3
的请求:
定义Token的刷新状态:
struct Token {
static let shared = Token()
/// 是否正在刷新
let refreshStatus = BehaviorRelay(value: false)
}
封装业务接口,内部处理Token
刷新逻辑,当Token
正在刷新时,其他请求挂起(抛弃当前事件,由于订阅关系还存在,Token状态的改变会再次触发事件):
func request(url: String) -> Single<String> {
return Token.shared.refreshStatus
// 确保状态发生改变时触发
.distinctUntilChanged()
// 正在刷新时,丢弃此次请求
// 订阅关系还存在,可在下次状态变更时继续请求
.filter { !$0 }
// 转换为Single,保证完成请求后失去订阅关系
.first()
// 请求业务接口
.flatMap { _ in
self._request(url: url)
}
...
}
根据业务的返回结果判断是否需要刷新Token
,并设置Token
的刷新状态(用返回数据为空模拟Token
失效的情况):
func request(url: String) -> Single<String> {
...
// 请求业务接口
.flatMap { _ in
self._request(url: url)
}
// 接口数据处理
.flatMap { data -> Single<String> in
// 错误处理:Token过期
if data.isEmpty {
Token.shared.refreshStatus.accept(true)
return self._request()
.flatMap { _ in
Token.shared.refreshStatus.accept(false)
return self._request(url: url)
}
}
// 正确处理:结果透传
return Single<String>.just(data)
}
}
此时,第一种情况已解决,当R1
正在刷新Token
时,其他请求暂时挂起,只有Token
状态发生改变,且只有未刷新时继续请求;

然而,第二种情况并未解决,等同于R1
、R2
和R3
同时发起请求,此时三个请求获得到的Token
状态都是未刷新的,所以都会进入刷新Token
的逻辑中,造成重复刷新Token
或边刷新边过期的情况;
解决思路也很简单,当第一个请求准备刷新Token
时,其他请求要等待前者刷新Token
完毕,由于三个请求可能归属于不同线程,涉及到Token
刷新状态的资源争夺,所以可以增加一个线程锁来解决此问题:
func request(url: String) -> Single<String> {
...
// 接口数据处理
.flatMap { data -> Single<String> in
// 错误处理:Token过期
if data.isEmpty {
// 将Token刷新状态和刷新逻辑加锁
defer { self.lock.unlock() }
self.lock.lock()
// 没有刷新,则开始刷新
if !Token.shared.refreshStatus.value {
Token.shared.refreshStatus.accept(true)
return self._request()
.flatMap { _ in
Token.shared.refreshStatus.accept(false)
return self._request(url: url)
}
}
// 注意!!!此处是递归哦
return self.request(url: url)
}
// 正确处理:结果透传
return Single<String>.just(data)
}
}
}
如上所示,在Token
过期的处理逻辑中加入线程锁,保证同一时刻内仅有一个线程可以刷新Token
,并将Token
状态置为正在刷新,但是线程锁不能保证Token
刷新完毕,所以如上有个递归,此处鸣谢大「明顺」,关键时刻点醒了我!注意是递归哦!
重点:此时的逻辑,总结为R1
争取到线程锁,进入刷新Token
逻辑,重置Token
刷新状态为True
,而R2
和R3
进入递归后挂起(原理同第一种情况,Token
正在刷新),之后R1
刷新Token
完毕后,重置Token
刷新状态为False
,恢复R2
和R3
的请求;
至此结束,完成整个刷新流程!
以下为完整代码:
struct Token {
/// 保证单次刷新
static let lock = NSRecursiveLock()
/// 是否正在刷新
static let refreshStatus = BehaviorRelay(value: false)
}
class LogicService {
static let shared = LogicService()
let taskQueue = DispatchQueue(label: "logic", attributes: .concurrent)
func request(url: String) -> Single<String> {
return Token.refreshStatus
.distinctUntilChanged()
// 正在刷新的等待,丢弃信号
.filter { !$0 }
.first()
// 请求业务接口
.flatMap { _ in
self._request(url: url)
}
// 接口数据处理
.flatMap { data -> Single<String> in
// 错误处理:Token过期
// 需要刷新
if data.isEmpty {
defer {
print("\(url) 解锁")
Token.lock.unlock()
}
print("\(url) 加锁")
Token.lock.lock()
// 没有刷新,则开始刷新
if !Token.refreshStatus.value {
print("\(url) 准备刷新Token")
Token.refreshStatus.accept(true)
return self._request(tag: url)
.flatMap { _ in
print("\(url) 刷新Token完毕")
Token.refreshStatus.accept(false)
return self._request(url: url)
}
}
print("\(url) 未刷新Token,请求业务接口")
return self.request(url: url)
}
// 正确处理:结果透传
return Single<String>.just(data)
}
}
}
extension LogicService {
func _request(tag: String) -> Single<Bool> {
return Single.create { [weak self] observer in
let item = DispatchWorkItem {
print("response: \(tag) Token刷新完毕")
tokenable = true
observer(.success(true))
}
print("request: \(tag) 正在刷新Token...")
self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)
return Disposables.create {
item.cancel()
}
}
}
func _request(url: String) -> Single<String> {
return Single.create { [weak self] observer in
let item = DispatchWorkItem {
if tokenable {
print("response: 数据请求完毕:\(url)")
observer(.success(url))
} else {
print("response:Token过期:\(url)")
observer(.success(""))
}
}
print("request: 正在请求数据:\(url)")
self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)
return Disposables.create {
item.cancel()
}
}
}
}
var tokenable: Bool {
get {
UserDefaults.standard.bool(forKey: "token")
}
set {
UserDefaults.standard.set(newValue, forKey: "token")
}
}
以下为R1
、R2
和R3
同时请求时的打印顺序:
request: 正在请求数据:R1
request: 正在请求数据:R2
request: 正在请求数据:R3
response:Token过期:R1
R1 加锁
R1 准备刷新Token
R1 解锁
request: R1 正在刷新Token...
response:Token过期:R2
R2 加锁
R2 未刷新Token,请求业务接口
R2 解锁
response:Token过期:R3
R3 加锁
R3 未刷新Token,请求业务接口
R3 解锁
response: R1 Token刷新完毕
R1 刷新Token完毕
request: 正在请求数据:R3
request: 正在请求数据:R2
request: 正在请求数据:R1
response: 数据请求完毕:R3
request token: R3 <NSThread: 0x60000331bfc0>{number = 8, name = (null)}
response: 数据请求完毕:R2
response: 数据请求完毕:R1
request token: R1 <NSThread: 0x6000033f5d40>{number = 7, name = (null)}
request token: R2 <NSThread: 0x60000331bfc0>{number = 8, name = (null)}
感谢我大团队,以上解决方案是大家一起努力的成果,前人栽树后人乘凉,而我只是其中一个受益者。欢迎斧正!!