·更新于 2026年10月25日· 10 次浏览 Node.jsRedis微信小程序工程实践BUG解决

微信小程序 access_token 总被限频:我用 Redis 锁和过期缓冲把刷新收成单飞

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

微信小程序 access_token 总被限频:我用 Redis 锁和过期缓冲把刷新收成单飞 封面
ZHOUYI

作者

ZHOUYI

这个问题一开始很隐蔽:小程序后端偶尔报 access_token 无效,重试后又好了。排查后发现,只要多个 Node.js 请求同时判断 token 过期,它们会各自去微信刷新。新 token 一出来,旧 token 就可能失效,结果并发请求互相覆盖,业务端看到的就是时好时坏,还容易触发接口限频。最开始我以为是微信接口不稳定,后来才发现是刷新逻辑太散:每个请求都认为自己该负责刷新。

为什么会越刷越乱

  • 多个服务实例或请求各自读本地缓存,过期时间不一致。
  • 到期瞬间同时触发刷新,形成并发刷 token。
  • 刷新期间,其他请求仍拿旧 token 调接口,导致失败。

单飞刷新:只让一个请求去拿新 token

我把 access_token 统一放进 Redis,并加一个短生命周期的刷新锁。谁先拿到锁,谁负责调用微信接口;没拿到锁的请求不重复刷,只稍微等待后重新读缓存。这样并发刷新被收成一次“单飞”,避免多个新 token 互相挤掉,也让失败更容易定位。锁的过期时间不要太长,只要能覆盖一次刷新请求即可;等待方也要限制重试次数,避免无限递归。

过期缓冲和失败回退

官方有效期通常较长,但我不建议卡到最后一秒才刷。这里会提前一段缓冲时间,比如提前 5 分钟视为过期,给刷新留出重试空间。若刷新失败,优先返回仍在缓冲期内的旧 token,或短暂重试,避免把错误直接抛给业务。多实例部署时,也尽量让 Redis 缓存作为唯一事实来源,不要把 token 散落在每个服务本地内存里,不然发布、扩容和重启都会产生多套缓存。具体有效期和频率限制以官方文档为准。

关键代码

javascript
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 就会稳定很多。”

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?