微信小程序 access_token 总被限频:我用 Redis 锁和过期缓冲把刷新收成单飞
多个 Node.js 请求同时刷新 access_token,会把旧 token 挤失效。这里记录我用 Redis 分布式锁、提前过期缓冲和失败回退,把刷新收敛成一次单飞请求的做法。

作者
ZHOUYI
这个问题一开始很隐蔽:小程序后端偶尔报 access_token 无效,重试后又好了。排查后发现,只要多个 Node.js 请求同时判断 token 过期,它们会各自去微信刷新。新 token 一出来,旧 token 就可能失效,结果并发请求互相覆盖,业务端看到的就是时好时坏,还容易触发接口限频。最开始我以为是微信接口不稳定,后来才发现是刷新逻辑太散:每个请求都认为自己该负责刷新。
为什么会越刷越乱
- 多个服务实例或请求各自读本地缓存,过期时间不一致。
- 到期瞬间同时触发刷新,形成并发刷 token。
- 刷新期间,其他请求仍拿旧 token 调接口,导致失败。
单飞刷新:只让一个请求去拿新 token
我把 access_token 统一放进 Redis,并加一个短生命周期的刷新锁。谁先拿到锁,谁负责调用微信接口;没拿到锁的请求不重复刷,只稍微等待后重新读缓存。这样并发刷新被收成一次“单飞”,避免多个新 token 互相挤掉,也让失败更容易定位。锁的过期时间不要太长,只要能覆盖一次刷新请求即可;等待方也要限制重试次数,避免无限递归。
过期缓冲和失败回退
官方有效期通常较长,但我不建议卡到最后一秒才刷。这里会提前一段缓冲时间,比如提前 5 分钟视为过期,给刷新留出重试空间。若刷新失败,优先返回仍在缓冲期内的旧 token,或短暂重试,避免把错误直接抛给业务。多实例部署时,也尽量让 Redis 缓存作为唯一事实来源,不要把 token 散落在每个服务本地内存里,不然发布、扩容和重启都会产生多套缓存。具体有效期和频率限制以官方文档为准。
关键代码
async function getAccessToken(retry = 3) {
const cached = await redis.get('wx:token');
if (cached) return JSON.parse(cached);
if (!retry) throw new Error('token wait timeout');
const locked = await redis.set('wx:token:lock', '1', 'EX', 10, 'NX');
if (!locked) { await sleep(300); return getAccessToken(retry - 1); }
try {
const token = await fetchWxToken();
await redis.set('wx:token', JSON.stringify(token), 'EX', token.expires_in - 300);
return token;
} finally { await redis.del('wx:token:lock'); }
}踩坑小结
- 锁一定要带过期时间,防止刷新进程挂掉后死锁;释放锁时最好只删自己加的锁。
- 等待方不要立刻再次刷新,应该读缓存或短延迟重试,并设置最大等待次数。
- 缓存过期时间要小于实际有效期,并保留缓冲,避免卡点刷新。
- 监控刷新失败、等待次数和限频日志,别只靠用户反馈猜。
“这类问题本质不是接口难用,而是并发状态没收敛。把“谁刷新、何时刷、失败怎么办”写清楚,再把 token 缓存和锁放在共享存储里,access_token 就会稳定很多。”