access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token,现在微信提供了两个接口用于获取access_token:分别是GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET 和 POST https://api.weixin.qq.com/cgi-bin/stable_token ,以前只提供了前者。
官方建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务。
获取 Stable Access token有两种调用模式: 1. 普通模式,access_token 有效期内重复调用该接口不会更新 access_token,绝大部分场景下使用该模式;2. 强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token;该接口调用频率限制为 1万次 每分钟,每天限制调用 50w 次。
是不是觉得Stable Access token就完全够用了,不用考虑什么中控服务器了,最佳实践是仍然建议构建中控服务器,这样一是可以减少对微信服务器的访问流量,减少网络意外的发生,二是可以突破访问频次的限制。
下面说下构建中控服务器的一些注意事项。
其实中控服务器不用设计得很复杂,主要是要实现以下几个功能:
1 对于授权用户提供access_token(其实还有ticket等)的缓存服务,缓存没有或者过期则需要从微信获取新的access_token,有效期内,直接返回缓存内容
2 考虑并发锁机制,缓存有效时没有影响,缓存无效时,要访问微信服务器去获取新的access_token,注意这时候一定要进行加锁操作,不然在极端情况下,会造成大量无效操作,甚至返回过期的错误的结果,第一个获得锁的可以访问微信去拿token,其他的得不到锁,等待一段时间后从缓存中获取更新内容。
关于加锁操作可以再稍微详细地讲下:以nodejs编程为例:假设这个中控服务器只跑了一个服务进程,那么可以不依赖redis,利用原子操作就可以进行加锁。
var wxifcache={
updatetime:jargs.wxupdatetime,//最开始加载一个较早的时间,每次获取token时更新
token:"",
ticket:""
}
var sab = new SharedArrayBuffer(1);
var wxlock = new Uint8Array(sab);
wxlock[0]=0;
...
if (!Atomics.add(ta, 0, 1)) {
gettokenfromwx();
Atomics.add(ta, 0, -1));
res.json(wxifcache);
};
else {
Atomics.add(ta, 0, -1));
setTimeout(() => { res.json(wxifcache); }, "5000");
};
如果是跑在多个进程实例或者多个节点上,那么最好借助redis的分布式锁来进行加锁操作。单节点redis利用setnx加锁就够用了。
//setnx代码示例
const Redis = require('ioredis');
const redis = new Redis();
redis.setnx('wxlock',5, '1', (err, result) => {
if (err) { console.error(err);}
else {
if (result) {
gettokenfromwx();
res.json(wxifcache());
}
else {
setTimeout(() => { res.json(wxifcache()); }, "5000");
}
}
redis.quit();
多节点redis可以用redlock加锁。
//redlock代码示例
import Client from "ioredis";
import Redlock from "redlock";
const redisA = new Client({ host: "a.redis.example.com" });
const redisB = new Client({ host: "b.redis.example.com" });
const redisC = new Client({ host: "c.redis.example.com" });
const redlock = new Redlock(
// You should have one client for each independent redis node
// or cluster.
[redisA, redisB, redisC],
{
// The expected clock drift; for more details see:
// http://redis.io/topics/distlock
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
// The max number of times Redlock will attempt to lock a resource
// before erroring.
retryCount: 0,//不尝试retry
// the time in ms between attempts
retryDelay: 200, // time in ms
// the max time in ms randomly added to retries
// to improve performance under high contention
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
retryJitter: 200, // time in ms
// The minimum remaining time on a lock before an extension is automatically
// attempted with the `using` API.
automaticExtensionThreshold: 500, // time in ms
}
);
...
redlock.lock("wxlock", 5000, function (err, lock) {
// we failed to lock the resource
if (err) {
// ...
setTimeout(() => { res.json(wxifcache()); }, "5000");
}
// we have the lock
else {
// ...do something here...
gettokenfromwx();
res.json(wxifcache());
// unlock your resource when you are done
lock.unlock(function (err) {
// we weren't able to reach redis; your lock will eventually
// expire, but you probably want to log this error
console.error(err);
});
}
});
注意后面两段代码中的wxifcache()不是从内存中获得,而是从redis中获得,redis中的值由gettokenfromwx()设置。