布景
在网络数据交互中,为了识别用户的身份,一般会设置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)}
感谢我大团队,以上处理方案是我们一起尽力的成果,前人栽树后人乘凉,而我只是其中一个受益者。欢迎斧正!!