一、业务特点
很多第三方服务,都使用访问令牌来做访问验证,比如某度、某信的access token,主要特征如下:
1、令牌由第三方系统发放,用于访问第三方特定资源;
2、令牌存在有效期限,过期自动失效;
3、同时只存在一个有效令牌,重新获取后之前获取到的令牌将会过期(这一点不是每个平台都严格限制,我们假设严格限制)。
二、整体设计
系统主要逻辑,主要设计目标为尽可能让用户一次以尽可能快的速度获得令牌,同时也提供前端做自动刷新策略的依据,具体设计如下:
1、分布式缓存(暂不考虑本地缓存场景,有兴趣可以讨论)最新令牌,缓存有效期低于令牌有效期;
2、先从缓存中获取,缓存中不存在、已过期或要求强制刷新,则重新获取令牌。如果需要强制,刷新前手动删除缓存;
3、获取令牌前先添加分布式锁,保障只有一个线程在获取令牌;
4、假设未抢到分布式锁,则限时等待,同时监听广播等待唤醒,超时则尝试再次从缓存中获取,如果获取不到则返回再次获取标识;
5、抢到分布式锁的线程,获取到令牌后,加入缓存,并广播令牌事件,唤醒等待中线程;
6、前端根据返回状态,若返回再次获取标识或已获得令牌但使用时返回失效时,则尝试重试再次获取;
7、当业务量大,需要本地缓存令牌时,先尝试从本地获取,刷新时需要双删本地令牌,并广播删除令牌事件,通知其他服务删除过期本地令牌。
三、代码实现(以Java为例,基于分布式缓存半伪代码)
1、主程序设计:
// 定义在最前
private static final CountDownLatch latch = new CountDownLatch(1);
// 以下真正主程序开始
// 强制刷新,删除缓存,否则尝试先从缓存获取
if (isForceRefresh) {
delete("CacheKey");
} else {
accToken = getFromCache();
}
// 未从缓存中获取到,或强制刷新时,尝试获取最新token
if (StringUtils.isBlank(accToken)) {
Lock lock = getLock("LockKey");
try {
// 尝试获取分布式锁,未获取到锁时,快速失败,获取到锁时锁n秒(一般考虑访问到锁需要的时间)
boolean b = lock.tryLock(-1, n, TimeUnit.SECONDS);
if (b) {
// 从远程获取最新访问令牌
accToken = newToken();
}
else {
// 未抢到锁,尝试等待其他线程获取到后直接获取,这里等待两次
accToken = waitGet(1);
}
} catch (InterruptedException e) {
// just ignore
} finally {
if (lock.isHeldByCurrentThread()) {
if (lock.isLocked()) {
lock.unlock();
}
// 如果当前线程抢到锁,则无论是否成功都广播事件唤醒其他等待中线程
broadcastWakeupEvent("EvantKey");
}
} // end: finally
} // end: if (StringUtils.isBlank(accToken))
return 新令牌
2、waitGet 等待设计:
String accToken = null;
try {
// 使用latch等待n毫秒后,重新尝试从缓存获取
boolean isWakeup = latch.await(n, TimeUnit.MILLISECONDS);
accToken = getFromCache();
// 未获取到,不是被事件唤醒的,且等待次数非0,则尝试再次等待
if (!isWakeup && StringUtils.isBlank(accToken) && count > 0) {
accToken = waitGet(--count);
}
if (StringUtils.isBlank(accToken)) {
// 未获取到,返回前端重试标识
return retrySign();
}
} catch (InterruptedException e) {
// 异常打断,返回前端重试标识
return retrySign();
}
return accToken;
3、监听获取到token事件,唤醒等待线程
latch.countDown();
本作品的版权所有权归作者所有,受法律保护。未经作者书面许可,任何个人或组织均不得以任何形式使用、复制、修改、传播、展示或在未获得授权的情况下进行商业利用。