微信服务号的 access_token 存在多实例 / 多调用方共享、被其他调用方刷新后导致本地缓存提前失效的核心问题(官方明确:新 access_token 生成后,旧的会立即失效,而非等 2 小时过期)。处理的核心思路是:放弃 “固定 2 小时缓存”,改为 “主动校验有效性 + 原子化刷新 + 容错降级”,确保缓存的 access_token 始终可用。
一、核心问题拆解
- 共享性:
access_token是服务号全局唯一的,多个后台服务、第三方工具(如微信开发者工具、其他开发人员调试)都可能调用/cgi-bin/token接口刷新; - 即时失效:新
access_token生成后,旧的立即失效(哪怕距离生成不足 2 小时); - 缓存风险:本地缓存的
access_token可能因他人刷新而 “静默失效”,直接使用会返回40001(invalid credential)错误。
二、解决方案:四步保障机制
1. 第一步:放弃 “固定过期时间”,改为 “提前失效缓存”(减少失效概率)
官方规定 access_token 有效期 “最长 2 小时”,但实际可能被他人刷新,因此本地缓存时不按 2 小时设置过期,而是缩短至 1.5 小时(5400 秒):
- 理由:即使他人在 1.5 小时后刷新,本地缓存也已过期,会主动重新获取新 token,避免使用 “已失效的旧 token”;
- 实现:用 Redis(推荐)或本地缓存(单实例可用)存储,设置
expire = 5400秒,同时记录 token 的 “生成时间”(备用)。
示例(Redis 存储结构):
key: wx_service_access_token:{appid} # 按appid区分(多服务号场景)
value: "17_O9xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx"
expire: 5400秒 # 1.5小时
2. 第二步:原子化刷新 token(避免多实例并发重复刷新)
如果多个服务实例同时发现缓存过期,会并发调用微信接口刷新 token,导致 “重复刷新”(浪费接口调用次数,且可能生成多个无效 token)。需通过分布式锁保证 “同一时间只有一个实例能刷新 token”:
实现流程:
- 所有实例获取 token 时,先查 Redis 缓存:
- 缓存存在且未过期:直接返回;
- 缓存不存在 / 已过期:尝试获取分布式锁(锁的 key 建议为
wx_access_token_lock:{appid},过期时间设为 30 秒,避免死锁);
- 成功获取锁的实例:
- 调用微信接口
/cgi-bin/token刷新 token; - 刷新成功后,将新 token 存入 Redis(设置 1.5 小时过期);
- 释放锁;
- 调用微信接口
- 未获取到锁的实例:
- 等待 100-300ms 后,重新查询 Redis 缓存(此时锁持有者已存入新 token);
- 若等待后仍无缓存,可重试 2-3 次(避免极端情况)。
分布式锁选型:
- 推荐:Redis Redlock(高可用)、ZooKeeper 锁;
- 简化方案(单 Redis 实例):用
SET NX EX命令实现(原子操作):# 尝试获取锁(key=锁名,value=随机字符串,EX=30秒过期,NX=不存在才设置) SET wx_access_token_lock:wx12345678 NX EX 30
3. 第三步:主动校验 token 有效性(兜底处理 “提前失效”)
即使设置了 1.5 小时缓存,仍可能出现 “缓存未过期,但 token 已被他人刷新” 的情况(比如他人在 1 小时后刷新)。此时需在使用 token 前 / 使用失败后校验有效性:
方案 A:使用前预校验(推荐,减少接口报错)
调用微信「校验接口调用凭证」接口,快速判断 token 是否有效(该接口无调用次数限制):
- 接口地址:
https://api.weixin.qq.com/cgi-bin/get_api_domain_ip?access_token=ACCESS_TOKEN - 响应逻辑:
- 返回
{"ip_list":["xxx.xxx.xxx.xxx"]}:token 有效,直接使用; - 返回
{"errcode":40001,"errmsg":"invalid credential"}:token 失效,触发强制刷新。
- 返回
方案 B:使用失败后重试(兜底)
当调用其他接口(如发送模板消息、获取用户信息)返回 40001(invalid credential) 或 42001(access_token expired) 时:
- 标记当前缓存的 token 为 “无效”(立即删除 Redis 缓存);
- 触发一次 “强制刷新 token”(走第二步的原子化刷新流程);
- 重试原接口(最多重试 1 次,避免循环)。
注意:仅针对
40001/42001错误重试,其他错误(如40003无效 openid)无需重试。
4. 第四步:限制刷新频率(避免滥用接口)
微信 access_token 接口有调用频率限制(默认每日上限 1000 次,足够使用),但需避免因异常情况(如缓存穿透)导致高频刷新:
- 给刷新操作加 “限流”:同一 appid 每分钟最多刷新 1 次(用 Redis 记录刷新次数,超过则拒绝,返回缓存的 token 或报错);
- 刷新失败时的退避策略:若调用微信接口失败(如网络超时、微信服务器报错),不要立即重试,采用 “指数退避”(1 秒、3 秒、5 秒后重试,最多重试 3 次),避免无效请求。
三、完整流程梳理(闭环)

四、关键注意事项
- 多服务号场景:按
appid区分缓存 key 和锁 key(避免不同服务号的 token 互相覆盖); - 单实例 vs 多实例:
- 单实例:可简化分布式锁为 “本地锁”(如 Java 的
ReentrantLock); - 多实例(如微服务集群):必须用分布式锁(Redis/ZooKeeper),否则会出现并发刷新问题;
- 单实例:可简化分布式锁为 “本地锁”(如 Java 的
- 缓存选型:优先用 Redis(支持过期时间、分布式锁),避免用本地内存缓存(多实例无法共享,易导致重复刷新);
- 权限控制:严格保管
appid和appsecret,避免泄露给第三方(第三方可能恶意刷新 token)。
五、代码示例(Java 伪代码)
public class WxAccessTokenManager {
private static final String CACHE_KEY = "wx_service_access_token:%s"; // %s 替换为 appid
private static final String LOCK_KEY = "wx_access_token_lock:%s";
private static final int CACHE_EXPIRE = 5400; // 1.5小时
private static final int LOCK_EXPIRE = 30; // 锁过期时间30秒
private RedisTemplate redisTemplate;
private WxConfig wxConfig; // 存储 appid、appsecret
// 获取有效 access_token
public String getValidAccessToken() {
String appid = wxConfig.getAppid();
String cacheKey = String.format(CACHE_KEY, appid);
// 1. 先查缓存
String token = (String) redisTemplate.opsForValue().get(cacheKey);
if (token != null && checkTokenValid(token)) {
return token;
}
// 2. 缓存无效,获取分布式锁刷新
String lockKey = String.format(LOCK_KEY, appid);
String lockValue = UUID.randomUUID().toString();
boolean lockAcquired = false;
try {
// 尝试获取锁(NX=不存在才设置,EX=30秒过期)
lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE, TimeUnit.SECONDS);
if (lockAcquired) {
// 3. 再次查缓存(避免锁等待期间已被其他实例刷新)
token = (String) redisTemplate.opsForValue().get(cacheKey);
if (token == null || !checkTokenValid(token)) {
// 4. 调用微信接口刷新 token
token = refreshTokenFromWx();
// 5. 存入缓存
redisTemplate.opsForValue().set(cacheKey, token, CACHE_EXPIRE, TimeUnit.SECONDS);
}
return token;
} else {
// 未获取到锁,等待后重试
Thread.sleep(200);
return getValidAccessToken(); // 递归重试(或循环重试2-3次)
}
} finally {
// 释放锁(仅释放自己持有锁)
if (lockAcquired) {
String currentLockValue = (String) redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentLockValue)) {
redisTemplate.delete(lockKey);
}
}
}
}
// 校验 token 有效性(调用微信接口)
private boolean checkTokenValid(String token) {
String url = String.format("https://api.weixin.qq.com/cgi-bin/get_api_domain_ip?access_token=%s", token);
try {
String response = HttpUtil.get(url); // 简化的 HTTP 工具
JSONObject json = JSONObject.parseObject(response);
return json.getJSONArray("ip_list") != null; // 有 ip_list 则有效
} catch (Exception e) {
return false; // 网络异常视为无效
}
}
// 调用微信接口刷新 token
private String refreshTokenFromWx() {
String url = String.format(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
wxConfig.getAppid(), wxConfig.getAppsecret()
);
String response = HttpUtil.get(url);
JSONObject json = JSONObject.parseObject(response);
if (json.containsKey("access_token")) {
return json.getString("access_token");
} else {
throw new RuntimeException("刷新 access_token 失败:" + response);
}
}
// 业务接口调用示例(带失败重试)
public String callWxApi(String apiUrl) {
String token = getValidAccessToken();
String fullUrl = apiUrl + "?access_token=" + token;
try {
String response = HttpUtil.get(fullUrl);
JSONObject json = JSONObject.parseObject(response);
if (json.containsKey("errcode") && (json.getIntValue("errcode") == 40001 || json.getIntValue("errcode") == 42001)) {
// token 失效,强制刷新后重试
redisTemplate.delete(String.format(CACHE_KEY, wxConfig.getAppid()));
token = getValidAccessToken();
fullUrl = apiUrl + "?access_token=" + token;
return HttpUtil.get(fullUrl);
}
return response;
} catch (Exception e) {
throw new RuntimeException("调用微信接口失败", e);
}
}
}
总结
处理 access_token 提前失效的核心是 “不依赖固定过期时间,而是通过 “提前缓存过期 + 原子化刷新 + 有效性校验 + 失败重试” 形成闭环”,既保证了 token 的可用性,又避免了重复刷新和接口滥用。实际落地时,优先使用 Redis 实现缓存和分布式锁,再结合微信的校验接口兜底,可彻底解决 “他人刷新导致本地缓存失效” 的问题。
微信Token缓存失效应对方案

1562

被折叠的 条评论
为什么被折叠?



