背景:
- 最近项目使用到了微信开发,获取微信token是必须可少的步骤。Redis是存储token的最佳方案,所以把获取token的方法写在了一个公用的方法内,如下:
public class WeChatUtil {
public static String APPID;
public static String SECRET;
private static String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
@Value("${wechat.appid}")
public void setAPPID(String APPID) {
WeChatUtil.APPID = APPID;
}
@Value("${wechat.secret}")
public void setSECRET(String SECRET) {
WeChatUtil.SECRET = SECRET;
}
public static long EXPIRETIME = 7100;
/**
* 获取token
* @return
*/
public static String getWeixinToken(){
//从redis中获取token
String token = (String)RedisUtil.get("vbpWxToken");
//token不存在或者已经过期,重新调用微信接口获取token
if(StringUtils.isBlank(token)){
try {
String response = HttpUtil.requestGet(String.format(TOKEN_URL, APPID, SECRET), null);
JSONObject jsonobject = JSONObject.parseObject(response);
//如果取得token,那么就返回
if(jsonobject.getString("access_token") != null){
token = jsonobject.getString("access_token");
//将token写入redis
RedisUtil.set("vbpWxToken", token, EXPIRETIME);
}
System.out.println(DateUtil.getCurrentFormatDate(null) + ",获取新token:" + token);
} catch (Exception e) {
e.printStackTrace();
}
}
return token;
}
}
- 这个写法初看没问题,但是我们遇到一个BUG是当程序运行一段时间后,偶尔会出现redis中的token校验失败的问题。手工清除redis中的token后获取,可以正常运行一段时间,但是过一段时间问题又出现。思来想去,后来才发现有可能存在如下问题:
-
举一种情况。假如token已经失效,在redis中已经不存在。此时两个线程1、2同时先后调用这个方法,线程1先调用了微信接口获取token,在线程1还没写入redis的时候,线程2也调用了微信的接口获取redis,然后会出现以下情况。
image.png
解决方案:
1. 模拟单例双重检查
- 这个解决方式模拟了单例中的双重检查,似乎没有问题。但是在分布式负载均衡的部署中不能用。因为synchronized是内存同步,分布式环境中每台机器的内存都是独立的。
- 但是如果是在单机模式下,这个是方法是能用的。
public static String getWeixinToken(){
String token = (String)RedisUtil.get("vbpWxToken");
//第一重检查
if(StringUtils.isBlank(token)){
//加锁
synchronized (token){
//第二重检查
if(StringUtils.isBlank(token)){
try {
String response = HttpUtil.requestGet(String.format(TOKEN_URL, APPID, SECRET), null);
JSONObject jsonobject = JSONObject.parseObject(response);
//如果取得token,那么就返回
if(jsonobject.getString("access_token") != null){
token = jsonobject.getString("access_token");
RedisUtil.set("vbpWxToken", token, EXPIRETIME);
}
System.out.println(DateUtil.getCurrentFormatDate(null) + ",获取新token:" + token);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return token;
}
2. 独立定时JOB刷新token
- 我们最后采用的是一个独立的定时任务去刷新token,这种方法最简单,也最高效了。
定时任务代码如下:
@EnableScheduling
@Component
public class WeChatSchedule {
private static String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
/**
* 刷新微信token
* 第一次延迟1秒执行,当执行完后6500秒再执行
*/
@Scheduled(initialDelay = 1000, fixedDelay = 6500*1000 )
public void setWeixinToken(){
try {
String response = HttpUtil.requestGet(String.format(TOKEN_URL, APPID, SECRET), null);
JSONObject jsonobject = JSONObject.parseObject(response);
//如果取得token,那么就写入redis
if (jsonobject.getString("access_token") != null) {
String token = jsonobject.getString("access_token");
RedisUtil.set("vbpWxToken", token, EXPIRETIME);
System.out.println(DateUtil.getCurrentFormatDate("yyyy-MM-dd HH:mm:ss") + "=====获取token成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 获取token的方法变为单纯的读取redis
public static String getWeixinToken(){
String token = (String)RedisUtil.get("vbpWxToken");
return token;
}
- 注意这个定时任务是写在一个单独的job工程中,单机模式部署。
网友评论