微信公众号开发 - token获取(保证同一时间段内只请求一次)

微信公众号开发文章目录

1.微信公众号开发 - 环境搭建
2.微信公众号开发 - 配置表设计以及接入公众号接口开发
3.微信公众号开发 - token获取(保证同一时间段内只请求一次)
4.微信公众号开发 - 菜单按钮bean封装
5.微信公众号开发 - 创建菜单
6.微信公众号开发 - 事件处理和回复消息
7.微信公众号开发 - 发送Emoji表情

项目完整代码请访问github:https://github.com/liaozq0426/wx.git

获取微信公众号token的流程

由于微信公众号token在获取后需要过一段时间才会失效,且获取token的接口每日有调用次数限制,因此我们对获取的token需要做缓存处理,避免每次用到token时都访问微信远程服务器。我们获取token的逻辑流程图如下
图1.获取微信公众号token流程
1)当redis缓存和数据库不存在token时,或者token已经失效时,从微信远程服务器获取token,并将获取的token缓存至数据库中。
2)当redis缓存或数据库存在token时,且token未失效时,直接返回token。

如何保证同一时间段内只请求微信服务器一次?

在考虑高并发的情况下,同一时间段内可能会调用微信服务器接口多次,为了避免这种情况出现,首先我们想到的可能是对方法进行同步处理,也就是使用synchronized关键字

public synchronized String readAccessToken() {
		
}

但是对整个方法同步处理的话,效率较低。我们可以对token的类型(accessType)进行同步,保证获取同一种类型的token是同步行进的,并且同时结合AtomicInteger类型变量的原子性,保证同步的情况下,同一时间段内只请求微信服务器一次。
图2.如何保证同一时间段内只请求微信服务器一次
上图中
1)首先需要先定义一个AtomicInteger类型的变量,初始值 0
2)当开始调用微信远程接口之前,首先需要判断AtomicInteger变量的值是否为1,如果不为1,则说明此时没有其他的线程在请求微信接口,这时我们可以调用微信接口,但调用之前需要设置AtomicInteger的值为1,表示已经有线程在请求微信接口了;如果AtomicInteger变量值为1,说明已经有线程在请求微信接口,此时就不要再次请求微信接口,而是循环读取缓存中的token
3)当有线程获取token成功后,需要将AtomicInteger变量重新设置为0,并将新token更新至缓存中。
4)对于accessType.intern()的理解,accessType为token的类型,在平时微信公众号开发中,常用的有access_tokenjsapi_ticket两种,前者为基础token,后者为使用jsapi时需要用到的token;intern()方法解释起来比较复杂,可以查阅资料了解一下。

wx_token表设计,存放token

CREATE TABLE `wx_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `platform` varchar(20) DEFAULT NULL COMMENT '公众号标识',
  `token_type` varchar(20) NOT NULL COMMENT 'token类型,access_token:基础token,jsapi_token:jsapi_ticket',
  `access_token` text NOT NULL COMMENT 'token值',
  `expires_in` int(11) NOT NULL COMMENT '失效时长',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `last_upd_time` timestamp NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后一次更新时间',
  `refresh_count` int(11) DEFAULT NULL COMMENT '刷新次数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;


微信公众号开发一般会用到两个token
1)access_token :是公众号的全局唯一接口调用凭据
2)jsapi_token:jsapi_ticket,是公众号用于调用微信JS接口的临时票据
我们可以将两种不同的token存储在一张表,便于维护

编写获取token的核心代码
由于获取token的代码较多,这里只展示了部分核心代码,如果想看完成代码,请通过github获取

以下是wx_token业务接口和实现类代码,其中WxTokenService中有4个接口,代码如下

package com.gavin.service;

import java.util.List;

import com.gavin.pojo.AccessToken;
import com.gavin.pojo.WxToken;
public interface WxTokenService {
	public List<WxToken> select(WxToken token) throws Exception;
	
	public WxToken selectOne(WxToken token) throws Exception;
	
	public int save(WxToken token) throws Exception;
	
	public AccessToken readAccessToken(String accessType , String platform) throws Exception;
}

其中前三个接口为查询和保存wx_token记录,最后一个方法readAccessToken比较复杂,会依次从redis缓存、数据库、微信服务器中获取token,只要任意一个步骤获取成功就返回token。

WxTokenServiceImpl实现类代码如下

package com.gavin.service.impl;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.gavin.cfg.RedisService;
import com.gavin.mapper.WxTokenMapper;
import com.gavin.pojo.AccessToken;
import com.gavin.pojo.Wechat;
import com.gavin.pojo.WxToken;
import com.gavin.service.WxCfgService;
import com.gavin.service.WxTokenService;
import com.gavin.util.WxUtil;

@Service
public class WxTokenServiceImpl implements WxTokenService , DisposableBean {
	
	private Logger logger = Logger.getLogger(this.getClass());
	
	public static final int DEFAULT_ACCESS_TOKEN_EXPIRESIN = 120;
	
	public static Map<String , AtomicInteger> tokenSyncMap = new ConcurrentHashMap<>();
	
	@Autowired
	private WxTokenMapper wxTokenMapper;
	
	@Autowired
	private RedisService redisService;
	
	@Autowired
	private WxCfgService wxCfgService;

	/**
	 * @title 查询token集合
	 * @author gavin
	 * @date 2019年11月27日
	 */
	@Override
	public List<WxToken> select(WxToken token) throws Exception {
		return wxTokenMapper.select(token);
	}

	/**
	 * @title 查询单个token
	 * @author gavin
	 * @date 2019年11月27日
	 */
	@Override
	public WxToken selectOne(WxToken token) throws Exception {
		List<WxToken> tokenList = select(token);
		if(tokenList.size() == 1)
			return tokenList.get(0);
		logger.info("查询结果集不符合预期");
		return null;
	}

	/**
	 * @title 保存token至数据库
	 * @author gavin
	 * @date 2019年11月27日
	 */
	@Override
	public int save(WxToken token) throws Exception {
		Integer id = token.getId();
		if(id != null && id > 0) {
			// 更新
			return this.wxTokenMapper.update(token);
		}else {
			// 新增
			return this.wxTokenMapper.insert(token);
		}
	}

	/**
	 * @title 读取微信token
	 * @author gavin
	 * @date 2019年11月27日
	 */
	@Override
	public AccessToken readAccessToken(String accessType, String platform) throws Exception {
		// 1.尝试从redis中读取
		AccessToken token = null;
		try {
			token = readAccessTokenByRedisAndDb(accessType , platform);
			if(token != null && !StringUtils.isBlank(token.getAccess_token()))
				return token;
			
			if(tokenSyncMap.get(accessType) != null && tokenSyncMap.get(accessType).get() > 0) {
				while(tokenSyncMap.get(accessType).get() > 0) {
					// 此时正在向微信服务器请求token,阻塞等待
					Thread.sleep(100);
					logger.info("正在向微信服务器请求token,阻塞等待...");
				}
				token = readAccessTokenByRedisAndDb(accessType , platform);
				if(token != null && !StringUtils.isBlank(token.getAccess_token()))
					return token;
				else
					return null;
			}else {					
				// 3.尝试从微信服务器上获取
				// 同步intern,保证在同一时间段内仅访问远程服务器一次
				String intern = accessType.intern();
				synchronized (intern) {					
					tokenSyncMap.put(accessType, new AtomicInteger(1));
					try {
						if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType) || AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) {			
							
							Wechat wechat = wxCfgService.selectWechat(platform);
							
							if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType)) {			
								logger.info("从微信服务器上获取access_token");
								token = WxUtil.getAccessToken(wechat.getBase64DecodeAppId(), wechat.getBase64DecodeAppSecret());
							}
							if(AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) {
								logger.info("从微信服务器上获取js_ticket");
								AccessToken Atoken = readAccessToken(AccessToken.TYPE_ACCESS_TOKEN , platform);
								token = WxUtil.getJSTicket(Atoken.getAccess_token());
							}
						}
						if(token != null && !StringUtils.isBlank(token.getAccess_token())) {				
							token.setAccess_type(accessType);
							// 缓存token
							cacheAccessToken(token , platform);
						}else {
							logger.error("从微信服务器上获取access_token失败");
						}
					} catch (Exception e) {
						logger.error("从微信服务器上获取access_token失败");
						logger.error(e.getMessage() , e);
					} finally {
						tokenSyncMap.get(accessType).decrementAndGet();
						logger.info("tokenSyncMap." + accessType + " count:" + tokenSyncMap.get(accessType).get());
					}
				}
				return token;
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new Exception("读取access_token失败");
		}
	}
	
	
	/**
	 * @title 从redis和数据库中读取accessToken
	 * @param accessType
	 * @return
	 */
	private AccessToken readAccessTokenByRedisAndDb(String accessType , String platform) {
		if(StringUtils.isBlank(accessType)) return null;
		String redisKey = null;
		if(!StringUtils.isBlank(accessType)) {
			// 生产redisKey
			redisKey = makeAccessTokenRedisKey(accessType , platform);
		}
		AccessToken token = null;
		try {
			Object obj = null;
			// 1.尝试从redis中读取
			logger.info("尝试从redis中读取...");
			obj = redisService.get(redisKey);
			if(obj != null) 
				token = (AccessToken) obj;
			if(token != null && !StringUtils.isBlank(token.getAccess_token())) {
				long tokenCreateTime = token.getCreate_time();
				logger.info("tokenCreateTime:" + tokenCreateTime);
				long interval = (System.currentTimeMillis() - tokenCreateTime) / 1000;
				logger.info("interval:" + interval);
				if(interval <= (token.getExpires_in() - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) {					
					return token;
				}else {
					logger.info("redis中的accessToken已经失效");
					redisService.del(redisKey);
				}
			}
		} catch (Exception e) {
			logger.error("尝试从redis中读取access_token失败");
			logger.error(e.getMessage() , e);
		}
		
		// 2.尝试从数据库中获取
		try {
			logger.info("尝试从数据库中读取...");
			WxToken wxTokenParam = new WxToken();
			wxTokenParam.setTokenType(accessType);
			wxTokenParam.setPlatform(platform);
			WxToken wxToken = this.selectOne(wxTokenParam);		
			if(wxToken != null) {
				// 判断token是否失效
				int expiresIn = wxToken.getExpiresIn();
				Date lastUpdTime = wxToken.getLastUpdTime();
				logger.info("System.currentTimeMillis:" + System.currentTimeMillis());
				logger.info("lastUpdTime:" + lastUpdTime.getTime() + ",format:" + lastUpdTime);
				long interval = (System.currentTimeMillis() - lastUpdTime.getTime()) / 1000;
				logger.info("interval:" + interval);
				if(interval <= (expiresIn - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) {
					token = new AccessToken();
					token.setAccess_token(wxToken.getAccessToken());
					token.setAccess_type(wxToken.getTokenType());
					token.setExpires_in(wxToken.getExpiresIn());
					token.setCreate_time(wxToken.getLastUpdTime().getTime());
					// 同步至redis
					// long redisExpires = System.currentTimeMillis() - wxToken.getLastUpdTime().getTime();
					long redisExpires = expiresIn - interval;
					redisService.set(redisKey, token , redisExpires);
					return token;	
				}
			}
			
		} catch (Exception e) {
			logger.error("尝试从数据库中读取access_token失败");
			logger.error(e.getMessage() , e);
		}
		return null;
	}
	
	/**
	 * @title 缓存access token,1.缓存至redis 2.缓存至数据库
	 * @author gavin
	 * @date 2019年5月23日
	 * @param accessToken
	 * @param platform
	 * @throws Exception
	 */
	public void cacheAccessToken(AccessToken accessToken , String platform) throws Exception {
		// 如果token的创建时间为空,则必须设置(从微信服务器获取到token时create_time为空)
		if(accessToken.getCreate_time() == 0) {			
			accessToken.setCreate_time(new Date().getTime());
			logger.info("设置token创建时间");
		}
		logger.info("accessToken_createTime:" + accessToken.getCreate_time());
		logger.info("System.currentTimeMillis:" + System.currentTimeMillis());
		// 1.缓存至redis
		String redisKey = null;
		String accessType = accessToken.getAccess_type();
		if(!StringUtils.isBlank(accessType)) {			
			redisKey = makeAccessTokenRedisKey(accessType , platform);
			
			redisService.set(redisKey, accessToken, accessToken.getExpires_in());
			logger.info("缓存" + platform + " " + accessType + "至redis成功");
			// 2.缓存至数据库
			WxToken tokenParam = new WxToken();
			tokenParam.setTokenType(accessType);
			tokenParam.setAccessToken(accessToken.getAccess_token());
			tokenParam.setExpiresIn(accessToken.getExpires_in());
			tokenParam.setPlatform(platform);
			// 1.先查询数据库中是否存在记录
			int result = 0;
			WxToken wxAccessToken = this.selectOne(tokenParam);
			if(wxAccessToken == null) {
				// 首次插入
				tokenParam.setRefreshCount(0);
				result = this.wxTokenMapper.insert(tokenParam);
			}else {
				// 更新
				if(wxAccessToken.getRefreshCount() == null) {
					tokenParam.setRefreshCount(1);
				}else {					
					tokenParam.setRefreshCount(wxAccessToken.getRefreshCount() + 1);
				}
				tokenParam.setId(wxAccessToken.getId());
				result = this.wxTokenMapper.update(tokenParam);
			}
			if(result == 1) 
				logger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库成功");
			else
				logger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库失败");
		}
	}
	
	/**
	 * @title access_token redis 缓存 key规则
	 * @author gavin
	 * @date 2019年5月23日
	 * @param accessType
	 * @param platform
	 * @return
	 */
	private String makeAccessTokenRedisKey(String accessType , String platform) {
		String redisKey = platform + "_" + accessType;
		return redisKey;
	}

	/**
	 * @title 销毁时清空缓存
	 * @author gavin
	 * @date 2019年11月27日
	 */
	@Override
	public void destroy() throws Exception {
		tokenSyncMap.clear();
		System.out.println("tokenSyncMap清空了,size:" + tokenSyncMap.size());
	}

}

猜你喜欢

转载自blog.csdn.net/u012693016/article/details/103240937
今日推荐