一次 Token 刷新并发 Bug 的排查与解决
1. 背景
最近在当牛马的时候,线上遇到一个 看似很常见、但实际很坑的 Bug。
场景
用户登录小程序后,正常使用一段时间,会突然被要求重新登录。
经过排查发现:
- 用户登录使用的是 token 机制
- token 有效期为 1 小时
- token 过期后会被 用户中心直接销毁
- 当前端携带一个 已过期 / 已销毁的 token 请求接口时,后端会直接返回
404 - 前端识别到 404 后,就会触发 重新登录流程
从用户视角来看:
用户体验问题
用着用着突然掉线,非常影响体验 😓
为了解决这个问题,我们决定引入 token 刷新机制:
- 在 token 即将过期时
- 后端主动调用 用户中心接口
- 使用旧 token 换取新 token
- 返回新 token 给前端继续使用
2. 初始实现思路
这个需求落到我身上,实现思路其实很直接:
- 每次请求先校验 token
- 如果 token 快过期(比如剩余 5 分钟)
- 调用用户中心接口,用 旧 token 换新 token
- 用户中心逻辑:
- 换新 token 成功
- 旧 token 立即失效
伪代码大概是这样:
1 | public Token validateAndRefresh(Token oldToken) { |
本地自测、Postman 测试一切正常,看起来问题已经解决。
然而——
一上环境就翻车了。
3. 线上问题现象
在测试环境发现:
- 有时候可以正常刷新
- 有时候还是会被要求重新登录
- 用
Postman单次请求 几乎必现成功 - 前端页面操作却是 时好时坏
进一步抓日志 + 抓请求后发现一个关键点:
关键现象
前端一次点击,会并发发起多次请求
也就是说:
- 第一次请求:
- 使用旧 token
- 成功刷新为新 token
- 旧 token 被用户中心销毁
- 第二、第三次请求:
- 仍然携带 已经被销毁的旧 token
- 直接 404
这就解释了为什么:
- “有时成功,有时失败”
- “一次点三次,只成功一次”
4. 第一版尝试方案(失败)
4.1 方案思路
我当时的第一反应是:
初始误判
既然只有第一次请求能换到新 token,那我把 新 token 和旧 token 的映射缓存起来 不就行了?
具体想法:
- 第一次请求刷新 token 成功后
- 在缓存中记录:
1 | oldToken -> newToken |
- 后续请求如果还带着 oldToken
- 就直接从缓存中拿 newToken
伪代码类似这样:
1 | public Token getToken(Token oldToken) { |
4.2 实际效果
改完后再测:
- 原来:一次点击 3 个请求,只成功 1 个
- 现在:一次点击 3 个请求,能成功 2 个
问题缓解了,但没有完全解决。
5. 关键问题定位
冷静下来重新分析后,发现这个方案本质上有一个致命问题:
致命问题
在第一个请求完成 token 刷新并写入缓存之前,其他并发请求已经进来了。
也就是说:
- 并发请求 A / B / C
- A 进入 refresh 流程
- B、C 在 A 写缓存之前就已经校验 token
- 此时:
- 旧 token 已经被用户中心销毁
- 缓存里还没有新 token
- 结果:B、C 直接失败
核心结论
这是一个典型的并发 + 线程安全问题,而不是单纯的缓存问题。
6. 最终解决方案
最终方案
通过分布式锁控制并发刷新,结合 L1 + L2 二级缓存共享最新 token,彻底解决并发请求下 token 被提前销毁的问题。
6.1 核心目标
- 同一时间 只允许一个请求刷新 token
- 其他并发请求:
- 不再直接刷新
- 而是等待 / 读取最新 token
6.2 方案设计
1. 分布式锁
- 使用
oldToken作为锁 key - 刷新 token 前先抢锁
- 保证只有一个请求会调用用户中心
2. 二级缓存
- L1 本地缓存(如 Caffeine / Guava)
- 极快
- 减少 Redis 压力
- L2 分布式缓存(如 Redis)
- 跨实例共享
- 存储最新 token
6.3 完整伪代码示例
1 | public Token getOrRefreshToken(Token oldToken) { |
7. 总结
这个 Bug 表面上看是:
表象问题
token 过期导致重新登录
但本质上是:
- 前端并发请求
- 后端 token 刷新非线程安全
- 外部系统(用户中心)对旧 token 立即失效
最终的经验总结:
- 凡是“刷新 / 更新 / 置换”类操作,一定要先想并发问题
- Postman 测不出的问题,前端并发一上来就原形毕露
- 分布式系统里:
- 锁
- Double Check
- 缓存层级
往往缺一不可。
希望这次踩坑记录,能帮到以后再踩类似坑的自己 🙃
吐槽
明明这个幂等应该用户中心实现的,结果还得我们做,离谱的很
评论








