上一篇文章咱們講了如何在負載均衡的項目中使用redis來緩存session數據,戳這裏。html
咱們在項目的進展過程當中,不只須要緩存session數據,有時候還須要緩存一些別的數據,好比說,微信的access_token.java
access_token是公衆號的全局惟一接口調用憑據,公衆號調用各接口時都需使用access_token。開發者須要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前爲2個小時,需定時刷新,重複獲取將致使上次獲取的access_token失效。nginx
一、建議公衆號開發者使用中控服務器統一獲取和刷新Access_token,其餘業務邏輯服務器所使用的access_token均來自於該中控服務器,不該該各自去刷新,不然容易形成衝突,致使access_token覆蓋而影響業務;
二、目前Access_token的有效期經過返回的expire_in來傳達,目前是7200秒以內的值。中控服務器須要根據這個有效時間提早去刷新新access_token。在刷新過程當中,中控服務器對外輸出的依然是老access_token,此時公衆平臺後臺會保證在刷新短期內,新老access_token均可用,這保證了第三方業務的平滑過渡;
三、Access_token的有效時間可能會在將來有調整,因此中控服務器不只須要內部定時主動刷新,還須要提供被動刷新access_token的接口,這樣便於業務服務器在API調用獲知access_token已超時的狀況下,能夠觸發access_token的刷新流程。
以上是微信開發文檔關於access_token的介紹,從上述的介紹能夠知道,access_token是一個很廣泛須要用到的,幾乎全部微信的接口都須要用到,顧名思義,token就是令牌的意思,這是微信服務器給開發者的令牌,有了這個令牌,
你才能作下一步的工做。
1.筆者以前的作法
我以前的作法很不經濟,就是上面說的第一條所反對的作法,每次須要訪問微信接口的時候,事先去獲取(也就是刷新)access_token。代碼以下。
1 String tokenStr = CommonUtil 2 .getTokenByUrl(ConfigUtil.TOKEN_URL); 3 JSONObject tokeJson = JSONObject.fromObject(tokenStr); 4 access_token = tokeJson.getString("access_token");
第一行中的ConfigUtil.TOKEN_URL爲微信的接口web
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+APPID+"&secret="+APP_SECRECT
getTokenByUrl方法就是一個普通的模擬http請求的方法,經過這個方法咱們能夠獲取到token數據,可是代價也是挺大的,假如我須要頻繁的調用微信接口,勢必會形成性能損失(每次模擬http請求都要時間,並且對微信服務器一種傷害)。
另一點就是,微信對access_token的請求是有限制的,當項目的流量的小的時候不要緊,可是若是流量多了,還用這種方法就會達到限制次數而被禁止訪問。
2.改進的作法
因爲項目中使用了nginx,若要作token的緩存的話,則必須作全局緩存,與session緩存的相似。
1)首先編寫抽象類AbstractBaseRedisDao,代碼以下。
package wonyen.mall.dao; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; public abstract class AbstractBaseRedisDao<K,V> { protected RedisTemplate<K, V> redisTemplate; public void setRedisTemplate(RedisTemplate<K, V> redisTemplate) { this.redisTemplate = redisTemplate; } protected RedisSerializer<String> getRedisSerializer(){ return redisTemplate.getStringSerializer(); } }
2)其次編寫實現類redisDao,代碼以下,該dao用於處理redis的數據庫中的鍵值數據對。redis
package wonyen.mall.dao; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.serializer.RedisSerializer; import redis.clients.jedis.Jedis; import wonyen.mall.constant.SystemConstant; public class RedisDao extends AbstractBaseRedisDao<String, HashMap<String, Object>> { /** * 新增鍵值對 * * @param key * @param value * @return
*/
public boolean addString(final String key, final String value) { boolean result = redisTemplate.execute(new RedisCallback<Boolean>() { public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = getRedisSerializer(); byte[] jkey = serializer.serialize(key); byte[] jvalue = serializer.serialize(value); return connection.setNX(jkey, jvalue); } }); return result; } /** * 新增(拼接字符串) * * @param key * @param value * @return
*/
public boolean appendString(final String key, final String value) { boolean result = redisTemplate.execute(new RedisCallback<Boolean>() { public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = getRedisSerializer(); byte[] jkey = serializer.serialize(key); byte[] jvalue = serializer.serialize(value); if (connection.exists(jkey)) { connection.append(jkey, jvalue); return true; } else { return false; } } }); return result; } /** * 新增(存儲Map) * * @param key * @param value * @return
*/
public String addMap(String key, Map<String, String> map) { Jedis jedis = getJedis(); String result = jedis.hmset(key, map); jedis.close(); return result; } /** * 獲取map * * @param key * @return
*/
public Map<String, String> getMap(String key) { Jedis jedis = getJedis(); Map<String, String> map = new HashMap<String, String>(); Iterator<String> iter = jedis.hkeys(key).iterator(); while (iter.hasNext()) { String ikey = iter.next(); map.put(ikey, jedis.hmget(key, ikey).get(0)); } jedis.close(); return map; } /** * 新增(存儲List) * * @param key * @param pd * @return
*/
public void addList(String key, List<String> list) { Jedis jedis = getJedis(); jedis.del(key); // 開始前,先移除全部的內容
for (String value : list) { jedis.rpush(key, value); } jedis.close(); } /** * 獲取List * * @param key * @return
*/
public List<String> getList(String key) { Jedis jedis = getJedis(); List<String> list = jedis.lrange(key, 0, -1); jedis.close(); return list; } /** * 新增(存儲set) * * @param key * @param set */
public void addSet(String key, Set<String> set) { Jedis jedis = getJedis(); jedis.del(key); for (String value : set) { jedis.sadd(key, value); } jedis.close(); } /** * 獲取Set * * @param key * @return
*/
public Set<String> getSet(String key) { Jedis jedis = getJedis(); Set<String> set = jedis.smembers(key); jedis.close(); return set; } /** * 刪除 (non-Javadoc) * * @see com.fh.dao.redis.RedisDao#delete(java.lang.String) */
public boolean delete(final String key) { boolean result = redisTemplate.execute(new RedisCallback<Boolean>() { public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = getRedisSerializer(); byte[] jkey = serializer.serialize(key); if (connection.exists(jkey)) { connection.del(jkey); return true; } else { return false; } } }); return result; } /** * 刪除多個 (non-Javadoc) * * @see com.fh.dao.redis.RedisDao#delete(java.util.List) */
public void delete(List<String> keys) { redisTemplate.delete(keys); } /** * 修改 (non-Javadoc) * * @see com.fh.dao.redis.RedisDao#eidt(java.lang.String, java.lang.String) */
public boolean eidt(String key, String value) { if (delete(key)) { addString(key, value); return true; } return false; } /** * 先刪除後添加 * @param key * @param value */
public void del_add(String key, String value){ delete(key); addString(key, value); } /** * 經過key獲取值 (non-Javadoc) * * */
public String get(final String keyId) { String result = redisTemplate.execute(new RedisCallback<String>() { public String doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = getRedisSerializer(); byte[] jkey = serializer.serialize(keyId); byte[] jvalue = connection.get(jkey); if (jvalue == null) { return null; } return serializer.deserialize(jvalue); } }); return result; } /** * 獲取Jedis * * @return
*/
public Jedis getJedis() { Properties pros = getPprVue(); String isopen = pros.getProperty("redis_isopen"); // 地址
String host = pros.getProperty("redis_hostName"); // 地址
String port = pros.getProperty("redis_port"); // 端口
String pass = pros.getProperty("redis_password"); // 密碼
if ("yes".equals(isopen)) { Jedis jedis = new Jedis(host, Integer.parseInt(port)); jedis.auth(pass); return jedis; } else { return null; } } /** * 讀取redis.properties 配置文件 * * @return * @throws IOException */
public Properties getPprVue() { InputStream inputStream = SystemConstant.class.getClassLoader() .getResourceAsStream("redis.properties"); Properties p = new Properties(); try { p.load(inputStream); inputStream.close(); } catch (IOException e) { // 讀取配置文件出錯
e.printStackTrace(); } return p; } }
3)redis的配置文件,redis.propertiesspring
redis_isopen:yes #主機地址 redis_hostName=xxx.xxx.xxx.xxx #端口 redis_port=6379 #密碼 redis_password=xxxxx #鏈接超時時間 redis_timeout=200000 redis_maxIdle=300 redis_maxActive=600 redis_maxWait=100000 redis_testOnBorrow=true
4)spring-redis配置文件數據庫
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd ">
<!-- session設置 -->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="3600"></property>
</bean>
<!-- redis鏈接池 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis_maxIdle}" />
<property name="testOnBorrow" value="${redis_testOnBorrow}" />
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
</bean>
<!-- redis鏈接工廠 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis_hostName}" />
<property name="port" value="${redis_port}" />
<property name="password" value="${redis_password}" />
<property name="timeout" value="${redis_timeout}" />
<property name="poolConfig" ref="poolConfig"></property>
</bean>
<!-- redisDao -->
<bean id="redisDao" class="wonyen.mall.dao.RedisDao">
<property name="redisTemplate" ref="redisTemplate" />
</bean>
<!-- redisAction -->
<bean id="redisAction" class="wonyen.mall.action.RedisAction" scope="prototype">
<property name="redisDao" ref="redisDao" />
</bean>
<!-- tokenScan -->
<bean id="tokenScan" class="wonyen.mall.scan.TokenScan">
<property name="redisDao" ref="redisDao" />
</bean>
</beans>
5)如今咱們必須開啓一個線程,讓它每隔一段時間(少於兩個小時)去從微信接口獲取新的token,而後存儲在redis的服務器中,線程類便是上述配置的rokenScan,以下所示。json
package wonyen.mall.scan; import net.sf.json.JSONObject; import wonyen.mall.dao.RedisDao; import wonyen.yipin.wechat.CommonUtil; import wonyen.yipin.wechat.ConfigUtil; /** * 掃描微信token的線程 * 啓動時候獲取token,而後每隔45分鐘刷新一次token * @author xdx * */
public class TokenScan implements Runnable{ public boolean run = true;// 線程開關
private static final int cycle=45;//刷新週期,45min刷新一次
private RedisDao redisDao; public void setRedisDao(RedisDao redisDao) { this.redisDao = redisDao; } @Override public void run() { while(run){ String tokenStr = CommonUtil.getTokenByUrl(ConfigUtil.TOKEN_URL); JSONObject tokeJson = JSONObject.fromObject(tokenStr); if (tokeJson.containsKey("access_token")) { String access_token=tokeJson.getString("access_token"); redisDao.del_add("access_token", access_token); String ticketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="
+ access_token + "&type=jsapi"; String ticketStr = CommonUtil.getTokenByUrl(ticketUrl); JSONObject ticketJson = JSONObject.fromObject(ticketStr); if(ticketJson.containsKey("ticket")){ String jsapi_ticket=ticketJson.getString("ticket"); redisDao.del_add("jsapi_ticket", jsapi_ticket); } //api_ticket
String apiTicketUrl="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+access_token+"&type=wx_card"; String apiTickerStr=CommonUtil.getTokenByUrl(apiTicketUrl); JSONObject apiTicketJson=JSONObject.fromObject(apiTickerStr); if(apiTicketJson.containsKey("ticket")){ String api_ticket=apiTicketJson.getString("ticket"); redisDao.del_add("api_ticket", api_ticket); } } try { Thread.sleep(cycle*60*1000);//3600000
} catch (InterruptedException e) { // TODO Auto-generated catch block
e.printStackTrace(); } } } }
在這個線程中,我不只僅把access_token存入redis,還將jsapi_ticket和apit_ticket也都存入了redis中,每隔45分鐘更新一次。咱們能夠專門作一個線程(與主項目分開)來執行這段代碼,這樣比較不會影響主項目的性能。api
6)讀取redis中的數據,咱們既然已經把token等數據放入了redis,接下來就是將他們取出來,很簡單,一樣是調用redisDao裏面的方法。緩存
package wonyen.yipin.service; import net.sf.json.JSONObject; import wonyen.mall.dao.RedisDao; import wonyen.yipin.wechat.CommonUtil; import wonyen.yipin.wechat.ConfigUtil; public class WxService { private RedisDao redisDao; public void setRedisDao(RedisDao redisDao) { this.redisDao = redisDao; } /** * 獲取微信jsapi_ticket * * @return
*/
public String getJsapiTicket() { String jsapi_ticket = redisDao.get("jsapi_ticket"); if (jsapi_ticket == null) { String access_token; if (redisDao.get("access_token") != null) { access_token = redisDao.get("access_token"); } else { String tokenStr = CommonUtil .getTokenByUrl(ConfigUtil.TOKEN_URL); JSONObject tokeJson = JSONObject.fromObject(tokenStr); access_token = tokeJson.getString("access_token"); } String ticketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="
+ access_token + "&type=jsapi"; String ticketStr = CommonUtil.getTokenByUrl(ticketUrl); JSONObject ticketJson = JSONObject.fromObject(ticketStr); if (ticketJson.containsKey("ticket")) { jsapi_ticket = ticketJson.getString("ticket"); } } return jsapi_ticket; } public String getApiTicket(){ String api_ticket = redisDao.get("api_ticket"); if(api_ticket == null){ String access_token; if(redisDao.get("access_token")!=null){ access_token = redisDao.get("access_token"); }else{ String tokenStr = CommonUtil .getTokenByUrl(ConfigUtil.TOKEN_URL); JSONObject tokeJson = JSONObject.fromObject(tokenStr); access_token = tokeJson.getString("access_token"); } String ticketUrl="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+access_token+"&type=wx_card"; String ticketStr=CommonUtil.getTokenByUrl(ticketUrl); JSONObject ticketJson = JSONObject.fromObject(ticketStr); if (ticketJson.containsKey("ticket")) { api_ticket = ticketJson.getString("ticket"); } } return api_ticket; } }
上述兩個方法分別是從redis中獲取jsapi_ticket和api_ticket的方法,咱們先從redis中直接去取,當取不到的時候咱們才調用原始的微信接口去取,這樣就不用頻繁的去請求微信接口了。更重要的一點,由於咱們用了redis,是全局的緩衝,在每一個負載均衡的分支上都是同步的。
咱們能夠看看redis的IDE中的數據。
![](http://static.javashuo.com/static/loading.gif)
雖然看不懂,可是他確實已經存進去了。