version 0.5
[TOC]
前言
前阶段公司的业务比较忙,很长时间没有整理出来什么博客.最近刚空出些时间,简单再整理一些记录.
产生原因:
客户端是被动向服务器查询登录状态,一些网络请求需要一个刷新token来验证客户端是否处于登录态,是则可以进行用户操作,否则做登出操作.
刚开始直接单纯的每个请求刷新token,刷新token,然后请求是没什么问题的.
但是随着版本迭代,任务增多,有些时候,比如app首次启动, 会进行一些列用户相关操作, 比如拉取用户信息, 拉取特定的活动项目,这样一个刷新token的操作可能会并发, 而我们的服务端刷新token每次可能都会不一样,这就产生了一些问题. 以下会展开示意.
之前的方案(并发刷新token)
为了简化理解我画了几张图,来说明情况,为了表示并发,我用RequestA,RequestB,RequestC分别表示三个请求,Server表示服务端
可以看见理想状态下,其实是没什么问题,请求都能正常收到与发送,但前提是他们是只有当A请求完全完成后B的后续请求,刷新token才不会受到干扰.
实际情况
然而,提到了随着业务增多,实际中大多请求都是并发的,于是乎就有可能有下面的情况.
可以看到,实际中,很有可能产生,A,B同时刷新token,而在A拿到新Token A去再一次请求时,B已经从服务器拿到了Token B导致了A请求又一次失败,随着并发的增多这种失败的可能性越来越多.
改进思路
我所想的是全局有一个单例的线程来掌管整个Token的刷新,并且这个token的刷新不是并发,而是队列,但是又不能让之后的请求变成队列. 否则简单的将所有请求变成队列即可,但实际情况我们根本不会让请求都是串行,无论从用户体验还是代码的书写上都是不好的.
所以在与iOS端讨论后,我们决定使用单一管理,并可阻塞的队列方式来管理token的请求过程,确保app内不会并发发送请求token的过程.
改进后就是主要几点:
- 当A请求过期后,需要向
TokenManager
去请求token.
TokenManager
会阻塞住队列,让后来的B请求等待.
- 当刷新完成后,通知所有队列中的对象,因为这个
TokenManager
只负责刷新返回token一个职责
- 所有请求拿到新的token,再来并发执行而互不影响.
代码概要
主要是实现一个任务队列,并要求阻塞, 因为刷新token也是一个异步请求,所以可以用wait()
来阻塞住,当一次请求完成后,使用notify()
来让队列继续执行,然后再加入一个超时规则,一段时间内,不会重新刷新token,加快之后的token请求
那么在安卓中,HandlerThread
内部已经有了一个loop的实现,就很方便处理这中情景,而不必要自己去写一些任务队列与Loop,简化代码量.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| tokenThread = new HandlerThread("token-handlerThread"); tokenThread.start();
mHandler = new Handler(tokenThread.getLooper()) { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg != null && msg.obj != null) { switch (msg.what) { case WITH_RETRY: doGetOrRefreshTokenWithRetry(msg.arg1, (RefreshTokenListener) msg.obj); break; case FORCE: doForceRefreshToken((RefreshTokenListener) msg.obj); break; case NORMAL: default: doGetOrRefreshToken((RefreshTokenListener) msg.obj); break; } } else { AILog.d(TAG, "getTokenHandler : msg is null"); }
} };
|
初始化Handler去操作不同的请求方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
private void doGetOrRefreshToken(RefreshTokenListener listener) { if (checkIsTokenExpired()) { if (mContext != null) { CountDownLatch latch = new CountDownLatch(1); MobileAccount.getInst().refreshToken(mContext, new MobileAccount.RefreshCallback() { @Override public void onSuccess(int code, String refreshedToken) { userUpload(refreshedToken); lastToken = refreshedToken; if (listener != null) { listener.onSuccess(refreshedToken); } lastRefreshTime = System.currentTimeMillis(); latch.countDown(); }
@Override public void doLogout(int code) { AILog.e(TAG, "doGetOrRefreshToken doLogout() called with: code = [" + code + "]"); if (listener != null) { listener.doLogout(code); } clear(); latch.countDown(); }
@Override public void onError(int errorCode) { AILog.e(TAG, "doGetOrRefreshToken onError() called with: errorCode = [" + errorCode + "]"); if (listener != null) { listener.onError(errorCode); } clear(); latch.countDown(); } }); waitUntilNotify(latch); } } else { listener.onSuccess(lastToken); }
}
|
当任务执行到访问网络刷新token时,通过信号量wait()
阻塞住任务,当收到回调时notify()
去执行,为了防止超时,内部起了一个定时器.(已弃用Lock方式,发现有极少情况可能在加锁之前请求已经过来,导致锁一直不释放)
采用CountDownLatch来阻塞,防止之前如果速度过快导致lock时已经notify的过程,同时提升可读性
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void waitUntilNotify(CountDownLatch latch) { if (latch != null) { if (latch.getCount() > 0) { try {
latch.await(REFRESH_EXPIRED_TIME, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } } }
|
阻塞当前线程,并等待一定时长,防止过多请求服务器导致token快速过期。以上是核心部分的简要说明。
外部通过RefreshTokenListener
来处理token的回调,做相应的处理.
相对于外部请求,依然是无感知TokenManager
的存在.
外部调用时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| TokenManager.getInstance().getToken(new TokenManager.RefreshTokenListener() { @Override public void onSuccess(String token) { }
@Override public void doLogOut(int code) {
}
@Override public void onError(int code) {
} });
|
更新说明:
版本 |
时间 |
说明 |
version 0.1 |
2019年03月20日11:29:20 |
初版 |
version 0.2 |
2019年03月22日09:56:52 |
修改TokenManager图表 |
version 0.3 |
2019年03月28日10:21:02 |
优化代码 |
version 0.4 |
2020年04月28日18:12:59 |
改用CountDownLatch简化 |
version 0.5 |
2022年02月08日17:49:01 |
更换图床为Github |