微信小程序電子發(fā)票中控服務(wù)實現(xiàn)方案

微信小程序電子發(fā)票中控服務(wù)實現(xiàn)方案

最近在開發(fā)微信小程序的時候,需要從微信服務(wù)定時獲取access_token与境⊙楹唬花了半天時間實現(xiàn)了一下這個功能。

默認(rèn)大家已經(jīng)熟練掌握以下技術(shù):

dubbo摔刁、SpringBoot挥转、Redis、Redisson共屈、分布式鎖

微信官方文檔中對access_token的說明:

1绑谣、建議開發(fā)者使用中控服務(wù)器統(tǒng)一獲取和刷新access_token,其他業(yè)務(wù)邏輯服務(wù)器所使用的access_token均來自于該中控服務(wù)器拗引,不應(yīng)該各自去刷新借宵,否則容易造成沖突,導(dǎo)致access_token覆蓋而影響業(yè)務(wù)矾削;

2壤玫、中控服務(wù)器需要根據(jù)這個有效時間提前去刷新access_token。在刷新過程中哼凯,中控服務(wù)器可對外繼續(xù)輸出的老access_token欲间,此時公眾平臺后臺會保證在5分鐘內(nèi),新老access_token都可用挡逼,這保證了第三方業(yè)務(wù)的平滑過渡括改;

3、Access_token的有效時間可能會在未來有調(diào)整家坎,所以中控服務(wù)器不僅需要內(nèi)部定時主動刷新嘱能,還需要提供被動刷新access_token的接口,這樣便于業(yè)務(wù)服務(wù)器在API調(diào)用獲知access_token已超時的情況下虱疏,可以觸發(fā)access_token的刷新流程惹骂。。

微信文檔地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183

方案要點設(shè)計:

  • access_token可以保存在緩存中做瞪,其定時過期可以使用redis來實現(xiàn)对粪。
  • 使用Redisson來連接redis,redis配置采用哨兵配置装蓬,配置1主2從3哨兵著拭。
  • 生產(chǎn)環(huán)境肯定不能用單節(jié)點去做中控服務(wù)器,需要用多臺服務(wù)來保證高可用牍帚,需要保證不沖突不覆蓋access_token儡遮。
  • 基于分布式的dubbo框架的provider服務(wù),為服務(wù)消費者consumer提供服務(wù)暗赶。部署多個provider服務(wù)實例鄙币,每一個provider服務(wù)實例都要配置定時任務(wù)從微信服務(wù)獲取access_token肃叶,每個provider服務(wù)實例都要與redis進(jìn)行交互。關(guān)鍵是需要使用分布式鎖來保證每兩個小時(實際用的是1個小時)的定時任務(wù)在觸發(fā)執(zhí)行時十嘿,只能有一臺服務(wù)成功運行定時任務(wù)從微信服務(wù)獲取access_token并保存到redis中因惭。

具體實現(xiàn)方案:

寫在前面

省略了在服務(wù)器搭建redis哨兵的過程。
該項目使用springboot框架绩衷。省略了springboot與duboo的集成過程蹦魔。
注意Redisson的版本問題,如果使用jdk1.8則可以使用Redisson的3.0以上的版本唇聘,比如3.7.1版姑。如果jdk是1.7則可以使用Redisson的2.9.1。
因為微信小程序后臺服務(wù)是在一臺老服務(wù)器上部署迟郎,jre只有1.7。所以這里選用Redisson的版本為2.9.1聪蘸。
SpringBoot對緩存的自動配置是其默認(rèn)的RedisTemplate宪肖,沒有對Redisson的自動配置,需要自己實現(xiàn)健爬。

手動實現(xiàn)SpringBoot的自動配置來配置Redisson

手動實現(xiàn)SpringBoot對Redisson的自動配置主要是分為兩步:

  • 1.將SpringBoot的配置文件對Redisson的配置項映射到配置Redisson的Properties類中控乾,主要是用注解@ConfigurationProperties(prefix = "redisson")
  • 2.將配置Redisson的Properties類的配置綁定到Redisson的自動配置類中,使其生效娜遵,主要是用注解@EnableConfigurationProperties(RedissonSentinelProperties.class)

application.properties中配置項

配置項中的ip是公司的內(nèi)網(wǎng)ip蜕衡,在用的時候改成自己的ip,密碼同理设拟。

redisson.masterName=mymaster
redisson.addSentinelAddress=192.168.10.112:26279,192.168.10.112:26280,192.168.10.114:26281
redisson.password=thirdservice******
redisson.readMode=slave
redisson.subscription-mode=slave
redisson.connectTimeout=3000
redisson.timeout=3000
redisson.idleConnectionTimeout=10000
redisson.retryAttempts=3
redisson.retryInterval=1500
redisson.reconnectionTimeout=3000
redisson.failedAttempts=3
redisson.slaveConnectionPoolSize=256
redisson.slaveConnectionMinimumIdleSize=64
redisson.masterConnectionPoolSize=256
redisson.masterConnectionMinimumIdleSize=64
redisson.subscriptionsPerConnection=5
redisson.pingTimeout=1000

配置Redisson的Properties類RedissonSentinelProperties

RedissonSentinelProperties 中使用@ConfigurationProperties(prefix = "redisson")綁定application.properties中以redisson開頭的配置項慨仿,并將自身添加到Spring容器中。

import lombok.Data;
import org.redisson.config.ReadMode;
import org.redisson.config.SubscriptionMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @Copyright (C), 2017-2018, 鋼的郭
 * @FileName: RedissonSentinelProperties
 * @Author: gangdeguo
 * @Date: 2018/11/26 10:50
 * @Description: Redisson哨兵配置資源項類
 * @Version:1.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonSentinelProperties {

    /**
     * (主服務(wù)器的名稱)
     * 主服務(wù)器的名稱是哨兵進(jìn)程中用來監(jiān)測主從服務(wù)切換情況的纳胧。
     */
    private String masterName;

    /**
     * (添加哨兵節(jié)點地址)
     * 可以通過host:port的格式來指定哨兵節(jié)點的地址镰吆。多個節(jié)點可以一次性批量添加。
     */
    private String addSentinelAddress;

    /**
     * (密碼)
     *      默認(rèn)值:null
     *      用于節(jié)點身份驗證的密碼跑慕。
     */
    private String password;

    /**
     * (DNS監(jiān)控間隔)
     *      單位:毫秒 默認(rèn)值:5000
     *      用來指定檢查節(jié)點DNS變化的時間間隔万皿。使用的時候應(yīng)該確保JVM里的DNS數(shù)據(jù)的緩存時間保持在足夠低的范圍才有意義。用-1來禁用該功能核行。
     */
    //private int dnsMonitoringInterval;

    /**
     * (讀取操作的負(fù)載均衡模式)
     *      默認(rèn)值: SLAVE(只在從服務(wù)節(jié)點里讀壤喂琛)
     *      注:在從服務(wù)節(jié)點里讀取的數(shù)據(jù)說明已經(jīng)至少有兩個節(jié)點保存了該數(shù)據(jù),確保了數(shù)據(jù)的高可用性芝雪。
     *      設(shè)置讀取操作選擇節(jié)點的模式减余。 可用值為: SLAVE - 只在從服務(wù)節(jié)點里讀取。 MASTER - 只在主服務(wù)節(jié)點里讀取绵脯。 MASTER_SLAVE - 在主從服務(wù)節(jié)點里都可以讀取佳励。
     */
    private ReadMode readMode;


    /**
     * (訂閱操作的負(fù)載均衡模式)
     *     默認(rèn)值:SLAVE(只在從服務(wù)節(jié)點里訂閱)
     *
     *     設(shè)置訂閱操作選擇節(jié)點的模式休里。 可用值為: SLAVE - 只在從服務(wù)節(jié)點里訂閱。 MASTER - 只在主服務(wù)節(jié)點里訂閱赃承。
     */
    private SubscriptionMode subscriptionMode;


    /**
     * (連接超時妙黍,單位:毫秒)
     *     默認(rèn)值:10000
     *
     *     同任何節(jié)點建立連接時的等待超時。時間單位是毫秒瞧剖。
     */
    private int connectTimeout;

    /**
     * (命令等待超時拭嫁,單位:毫秒)
     *     默認(rèn)值:3000
     *
     *     等待節(jié)點回復(fù)命令的時間。該時間從命令發(fā)送成功時開始計時抓于。
     */
    private int timeout;
    /**
     * (連接空閑超時做粤,單位:毫秒)
     *     默認(rèn)值:10000
     *
     *     如果當(dāng)前連接池里的連接數(shù)量超過了最小空閑連接數(shù),而同時有連接空閑時間超過了該數(shù)值捉撮,那么這些連接將會自動被關(guān)閉怕品,并從連接池里去掉。時間單位是毫秒巾遭。
     */
    private int idleConnectionTimeout;

    /**
     * (命令失敗重試次數(shù))
     *     默認(rèn)值:3
     *
     *     如果嘗試達(dá)到 retryAttempts(命令失敗重試次數(shù)) 仍然不能將命令發(fā)送至某個指定的節(jié)點時肉康,將拋出錯誤。如果嘗試在此限制之內(nèi)發(fā)送成功灼舍,則開始啟用 timeout(命令等待超時) 計時吼和。
     */
    private int retryAttempts;

    /**
     * (命令重試發(fā)送時間間隔,單位:毫秒)
     *     默認(rèn)值:1500
     *
     *     在一條命令發(fā)送失敗以后骑素,等待重試發(fā)送的時間間隔炫乓。時間單位是毫秒。
     */
    private int retryInterval;

    /**
     * (重新連接時間間隔献丑,單位:毫秒)
     *     默認(rèn)值:3000
     *
     *     當(dāng)與某個節(jié)點的連接斷開時末捣,等待與其重新建立連接的時間間隔。時間單位是毫秒阳距。
     */
    private int reconnectionTimeout;

    /**
     * (執(zhí)行失敗最大次數(shù))
     *     默認(rèn)值:3
     *
     *     在某個節(jié)點執(zhí)行相同或不同命令時塔粒,連續(xù) 失敗 failedAttempts(執(zhí)行失敗最大次數(shù)) 時,該節(jié)點將被從可用節(jié)點列表里清除筐摘,直到 reconnectionTimeout(重新連接時間間隔) 超時以后再次嘗試卒茬。
     */
    private int failedAttempts;

    /**
     * (從節(jié)點連接池大小)
     *     默認(rèn)值:64
     *
     *     多從節(jié)點的環(huán)境里咖熟,每個 從服務(wù)節(jié)點里用于普通操作(非 發(fā)布和訂閱)連接的連接池最大容量圃酵。連接池的連接數(shù)量自動彈性伸縮。
     */
    private int slaveConnectionPoolSize;

    /**
     * (從節(jié)點最小空閑連接數(shù))
     *     默認(rèn)值:32
     *
     *     多從節(jié)點的環(huán)境里馍管,每個 從服務(wù)節(jié)點里用于普通操作(非 發(fā)布和訂閱)的最小保持連接數(shù)(長連接)郭赐。長期保持一定數(shù)量的連接有利于提高瞬時讀取反映速度。
     */
    private int slaveConnectionMinimumIdleSize;

    /**
     * (主節(jié)點連接池大腥贩小)
     *     默認(rèn)值:64
     *
     *     主節(jié)點的連接池最大容量捌锭。連接池的連接數(shù)量自動彈性伸縮俘陷。
     */
    private int masterConnectionPoolSize;

    /**
     * (主節(jié)點最小空閑連接數(shù))
     *     默認(rèn)值:32
     *
     *     多從節(jié)點的環(huán)境里,每個 主節(jié)點的最小保持連接數(shù)(長連接)观谦。長期保持一定數(shù)量的連接有利于提高瞬時寫入反應(yīng)速度拉盾。
     */
    private int masterConnectionMinimumIdleSize;

    /**
     * ping超時時間
     */
    private int pingTimeout;
}

Redisson的自動配置類RedissonSentinelConfiguration

RedissonSentinelConfiguration是一個SpringBoot的配置類。該配置類中使用注解@EnableConfigurationProperties(RedissonSentinelProperties.class)來綁定和啟用上節(jié)的RedissonSentinelProperties類豁状。
配置類RedissonSentinelConfiguration 是沒有無參構(gòu)造器的捉偏,在項目啟動過程中,Spring會自動將其容器中的RedissonSentinelProperties類注入到RedissonSentinelConfiguration 中泻红。
配置類RedissonSentinelConfiguration 創(chuàng)建了RedissonClient 并將其注冊到Spring容器中夭禽,我么在使用的時候使用@Autowire注進(jìn)來就直接使用就可以了。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.codec.FstCodec;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Copyright (C), 2017-2018, 鋼的郭
 * @FileName: RedissonSentinelConfiguration
 * @Author: guoyfiang
 * @Date: 2018/11/26 10:50
 * @Description: Redisson哨兵自動配置類
 * @Version:1.0
 */
@Configuration
@EnableConfigurationProperties(RedissonSentinelProperties.class)
public class RedissonSentinelConfiguration {

    private RedissonSentinelProperties redissonSentinelProperties;

    public RedissonSentinelConfiguration(RedissonSentinelProperties redissonSentinelProperties) {
        this.redissonSentinelProperties = redissonSentinelProperties;
    }


    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        //Codec codec = new FstCodec();
        //config.setCodec(codec);
        SentinelServersConfig sentinelConfig = config.useSentinelServers();

        System.out.println(redissonSentinelProperties.getAddSentinelAddress() + "++++++++++++++");

        //設(shè)置哨兵主服務(wù)器的名稱
        sentinelConfig.setMasterName(redissonSentinelProperties.getMasterName());
        //添加哨兵節(jié)點地址
        sentinelConfig.addSentinelAddress(redissonSentinelProperties.getAddSentinelAddress().split(","));
        //設(shè)置redis哨兵連接密碼
        sentinelConfig.setPassword(redissonSentinelProperties.getPassword());
        //讀取操作的負(fù)載均衡模式
        sentinelConfig.setReadMode(redissonSentinelProperties.getReadMode());
        // 訂閱操作的負(fù)載均衡模式
        sentinelConfig.setSubscriptionMode(redissonSentinelProperties.getSubscriptionMode());
        // 設(shè)置redis連接超時時間
        sentinelConfig.setConnectTimeout(redissonSentinelProperties.getConnectTimeout());
        // 設(shè)置命令等待超時
        sentinelConfig.setTimeout(redissonSentinelProperties.getTimeout());
        // 設(shè)置連接空閑超時
        sentinelConfig.setIdleConnectionTimeout(redissonSentinelProperties.getIdleConnectionTimeout());
        // 設(shè)置命令失敗重試次數(shù)
        sentinelConfig.setRetryAttempts(redissonSentinelProperties.getRetryAttempts());
        // 設(shè)置命令重試發(fā)送時間間隔
        sentinelConfig.setRetryInterval(redissonSentinelProperties.getRetryInterval());
        // 設(shè)置重新連接時間間隔
        sentinelConfig.setReconnectionTimeout(redissonSentinelProperties.getReconnectionTimeout());
        // 設(shè)置執(zhí)行失敗最大次數(shù)
        sentinelConfig.setFailedAttempts(redissonSentinelProperties.getFailedAttempts());
        // 設(shè)置從節(jié)點連接池大小
        sentinelConfig.setSlaveConnectionPoolSize(redissonSentinelProperties.getSlaveConnectionPoolSize());
        // 設(shè)置從節(jié)點最小空閑連接數(shù)
        sentinelConfig.setSlaveConnectionMinimumIdleSize(redissonSentinelProperties.getSlaveConnectionMinimumIdleSize());
        // 設(shè)置主節(jié)點連接池大小
        sentinelConfig.setMasterConnectionPoolSize(redissonSentinelProperties.getMasterConnectionPoolSize());
        // 主節(jié)點最小空閑連接數(shù)
        sentinelConfig.setMasterConnectionMinimumIdleSize(redissonSentinelProperties.getMasterConnectionMinimumIdleSize());
        // 單個連接最大訂閱數(shù)量
        sentinelConfig.setSubscriptionsPerConnection(redissonSentinelProperties.getSubscriptionsPerConnection());
        // ping超時時間
        sentinelConfig.setPingTimeout(redissonSentinelProperties.getPingTimeout());
        return Redisson.create(config);
    }
}

與redis進(jìn)行交互的service接口

定義接口主要用于定義api谊路,dubbo項目的consumer 和provider同時依賴它讹躯。
此接口主要是用于使用Redisson與緩存進(jìn)行交互。

import cn.leadeon.thirdparty.base.ResultData;

/**
 * @Copyright (C), 2017-2018, 鋼的郭
 * @FileName: ThirdCacheService
 * @Author: Leadeon
 * @Date: 2018/11/27 14:39
 * @Description: 第三方緩存交互service
 * @Version:1.0
 */
public interface ThirdCacheService {
    
    /**
     * 保存數(shù)據(jù)至緩存
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @param expireTime 過期時間
     * @return ResultData
     */
     ResultData<?> set(String trace, String key, String value, int expireTime);
    /**
     * 
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @return ResultData
     */
     ResultData<?> set(String trace, String key, String value);
    
    
    /**
     * 獲取緩存的的value
     * @param trace 流水號
     * @param key 鍵
     * @return ResultData
     */
     ResultData<?> get(String trace, String key);
    
    /**
     * INCR key 
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @return ResultData
     */
     ResultData<?> incrBy(String trace, String key, Double value);
    
    /**
     * INCR key 
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @return ResultData
     */
     ResultData<?> incrBy(String trace, String key, Double value, long expireTime);
    
    /**
     * DECR key 
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @return ResultData
     */
     ResultData<?> decrBy(String trace, String key, Double value);

    
    /**
     * 向sortset中插入元素
     * @param trace 流水號
     * @param key 鍵
     * @param member 成員
     * @param score 得分
     * @return ResultData
     */
     ResultData<?> zadd(String trace, String key, String member, Long score);


    /**
     * 獲取介于最大值和最小值之間的固定條數(shù)據(jù)
     * @param trace 流水號
     * @param key 鍵
     * @param startScore  開始得分
     * @param endScore  結(jié)束得分
     * @param offset
     * @param count
     * @return ResultData
     */
     ResultData<?> zRangeByScore(String trace, String key, long startScore, long endScore, int offset, int count);

    /**
     * 獲取介于最大值和最小值之間的總條數(shù)
     * @param trace 流水號
     * @param key 鍵
     * @param startScore  開始
     * @param endScore  結(jié)束
     * @return ResultData
     */
     ResultData<?>  zCount(String trace, String key, long startScore, long endScore);
    
    /**
     * 入消息隊列
     * @param trace 流水號
     * @param key 鍵
     * @param value 值
     * @return ResultData
     */
     ResultData<?> lPush(String trace, String key, String value);
    
    /**
     * 出消息隊列
     * @param trace 流水號
     * @param key 鍵
     * @return ResultData
     */
     ResultData<?> rPop(String trace, String key);
    
    /**
     * 獲取分布式鎖
     * @param trace 流水號
     * @param key 鍵
     * @param expireTime 過期時間
     * @return ResultData
     */
     ResultData<?> getLock(String trace, String key, long expireTime);
    
    /**
     * 釋放分布式鎖
     * @param trace 流水號
     * @param key 鍵
     * @return ResultData
     */
     ResultData<?> unLock(String trace, String key);
    
}

與redis進(jìn)行交互的service實現(xiàn)類

實現(xiàn)了上一節(jié)的接口缠劝,與緩存進(jìn)行交互

import cn.leadeon.thirdparty.base.ResultData;
import cn.leadeon.thirdparty.constant.ThridPartyResCode;
import cn.leadeon.thirdparty.log.Log;
import org.redisson.api.RAtomicDouble;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/**
 * @Copyright (C), 2017-2018, 鋼的郭
 * @FileName: RedissonSentinelConfiguration
 * @Author: guoyfiang
 * @Date: 2018/11/26 10:50
 * @Description: Redisson哨兵自動配置類
 * @Version:1.0
 */
@Component
public class ThirdCacheServiceImpl implements ThirdCacheService {


    public final static Log log = new Log(ThirdCacheServiceImpl.class);

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public ResultData<?> set(String trace, String key, String val, int expireTime) {
        ResultData<Boolean> rd = new ResultData<>();
        try {
            redissonClient.getBucket(key).set(val, expireTime, TimeUnit.SECONDS);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(true);
        } catch (Exception e) {
            rd.setResultData(false);
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setDesc("get data fail..");
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "set [key:" + key + "val:" + val + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> set(String trace, String key, String val) {
        ResultData<Boolean> rd = new ResultData<>();
        try {

            redissonClient.getBucket(key).set(val);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(true);
        } catch (Exception e) {
            rd.setResultData(false);
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setDesc("get data fail..");
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "set [key:" + key + "val:" + val + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> get(String trace, String key) {
        ResultData<Object> rd = new ResultData<>();
        try {
            RBucket<Object> bucket = redissonClient.getBucket(key);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(bucket.get());
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setDesc("get data fail..");
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "get [key:" + key + "] fail", e);
        }
        return rd;
    }


    @Override
    public ResultData<?> incrBy(String trace, String key, Double value) {
        ResultData<Double> rd = new ResultData<Double>();
        try {
            double bucket = redissonClient.getAtomicDouble(key).addAndGet(value);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(bucket);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "incrBy [key:" + key + "score:" + value + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> incrBy(String trace, String key, Double value, long expireTime) {
        ResultData<Double> rd = new ResultData<Double>();
        try {
            double bucket = redissonClient.getAtomicDouble(key).addAndGet(value);
            redissonClient.getAtomicDouble(key).expire(expireTime, TimeUnit.MILLISECONDS);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(bucket);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "incrBy [key:" + key + "score:" + value + "] fail", e);
        }
        return rd;
    }


    @Override
    public ResultData<?> decrBy(String trace, String key, Double value) {
        ResultData<Double> rd = new ResultData<Double>();
        try {
            RAtomicDouble atomicDouble = redissonClient.getAtomicDouble(key);
            if (value <= atomicDouble.get()) {
                //可扣減
                double bucket = redissonClient.getAtomicDouble(key).addAndGet(-value);
                rd.setResultData(bucket);
                rd.setResultCode(ThridPartyResCode._0000);
            } else {
                //余額不夠扣減
                rd.setResultCode(ThridPartyResCode._0005);
                rd.setResultData(atomicDouble.get());
            }
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "decrBy [key:" + key + "score:" + value + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> zadd(String trace, String key, String member, Long score) {

        ResultData<Boolean> rd = new ResultData<Boolean>();
        try {
            redissonClient.getScoredSortedSet(key).add(score, member);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(true);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setResultData(false);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "zadd [key:" + key + "member:" + member + "score:" + score + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> zRangeByScore(String trace, String key, long startScore, long endScore, int offset, int count) {
        ResultData<ArrayList<String>> rd = new ResultData<ArrayList<String>>();
        ArrayList<String> resList = new ArrayList<String>();
        try {
            Collection<Object> resData = redissonClient.getScoredSortedSet(key).valueRangeReversed(startScore, true, endScore, true, offset, count);
            Iterator<Object> iterator = resData.iterator();
            while (iterator.hasNext()) {
                resList.add(String.valueOf(iterator.next()));
            }
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(resList);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "zRangeByScore [key:" + key + "startScore:" + startScore + "endScore:" + endScore + "offset:" + offset + "count:" + count + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> zCount(String trace, String key, long startScore, long endScore) {
        ResultData<Long> rd = new ResultData<Long>();
        try {
            Long count = redissonClient.getScoredSortedSet(key).count(startScore, true, endScore, true);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(count);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "zCount [key:" + key + "startScore:" + startScore + "endScore:" + endScore + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> lPush(String trace, String key, String value) {
        ResultData<Boolean> rd = new ResultData<Boolean>();
        try {
            redissonClient.getDeque(key).add(value);
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(true);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setResultData(false);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "zadd [key:" + key + "value:" + value + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> rPop(String trace, String key) {
        ResultData<Object> rd = new ResultData<Object>();
        try {
            Object value = redissonClient.getDeque(key).pollLast();
            rd.setResultCode(ThridPartyResCode._0000);
            rd.setResultData(value);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setResultData(null);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "zadd [key:" + key + "] fail", e);
        }
        return rd;
    }

    @Override
    public ResultData<?> getLock(String trace, String key, long expireTime) {
        ResultData<Boolean> rd = new ResultData<Boolean>();
        Boolean result = false;
        try {
            RLock lock = redissonClient.getReadWriteLock(key).writeLock();
            result = lock.tryLock(1000, expireTime, TimeUnit.MILLISECONDS);
            rd.setResultCode(ThridPartyResCode._0000);
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "getLock [key:" + key + "] fail", e);
        }
        rd.setResultData(result);
        return rd;
    }

    @Override
    public ResultData<?> unLock(String trace, String key) {
        ResultData<Boolean> rd = new ResultData<Boolean>();
        Boolean result = false;
        try {
            redissonClient.getReadWriteLock(key).writeLock().unlock();
            rd.setResultCode(ThridPartyResCode._0000);
            result = true;
        } catch (Exception e) {
            rd.setResultCode(ThridPartyResCode._0001);
            rd.setException(e);
            // 打印錯誤日志
            log.error("trace:" + trace + "unLock [key:" + key + "] fail", e);
        }
        rd.setResultData(result);
        return rd;
    }
}

獲取微信AccessToken的service的接口GetAccessTokenService

該接口定義了主動與被動獲取access_token方法的api蜀撑,duboo項目的consumer與provider都依賴它。

package cn.leadeon.thirdparty.service;

import cn.leadeon.thirdparty.base.ResultData;

/**
 * @Copyright (C), 2015-2018, 鋼的郭
 * @FileName: GetAccessTokenService
 * @Author: gangdeguo
 * @Date: 2018-11-27 11:45
 * @Description: 獲取微信電子發(fā)票access_token
 * @Version: 1.0
 */
public interface GetAccessTokenService {
    // 從緩存獲取access_token剩彬,如果緩存中不存在則從微信服務(wù)獲取access_token
    ResultData<?> getAccessToken(String trace , String busCode);
    // 從微信服務(wù)獲取access_token,存到緩存中
    void setAccessToken(String trace , String busCode);
}

獲取微信電子發(fā)票 access_token service實現(xiàn)類GetAccessTokenServiceImpl

該接口實現(xiàn)類,實現(xiàn)了上節(jié)接口定義的方法矿卑。具備主動與被動獲取access_token的能力喉恋。
這個類是最核心的代碼。使用分布式的鎖來保證所有provider實例的定時任務(wù)觸發(fā)后只能有一個provider實例的定時方法能從微信服務(wù)獲取access_token并保存到緩存中母廷。并且一旦有一個provider實例成功從微信服務(wù)獲取access_token并保存到緩存中轻黑,那么接下來的半小時所有的provider實例都不用去請求微信服務(wù)來再次獲取access_token。避免頻繁請求微信服務(wù)琴昆。

該接口實現(xiàn)類中使用SpringBoot的定時任務(wù)需要在項目的主啟動類上標(biāo)注解@EnableScheduling開啟定任務(wù)

package cn.leadeon.thirdparty.service;

import cn.leadeon.thirdparty.base.ResultData;
import cn.leadeon.thirdparty.common.http.LocalHttpClient;
import cn.leadeon.thirdparty.constant.ThridPartyResCode;
import cn.leadeon.thirdparty.log.Log;
import cn.leadeon.thirdparty.pojo.AccessToken;
import com.alibaba.fastjson.JSON;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @Copyright (C), 2015-2018, 鋼的郭
 * @FileName: GetAccessTokenServiceImpl
 * @Author: gangdeguo
 * @Date: 2018-11-27 11:47
 * @Description: 獲取微信電子發(fā)票 access_token  service實現(xiàn)類
 * @Version: 1.0
 */
@Service
public class GetAccessTokenServiceImpl implements GetAccessTokenService {

    /**
     * 分布式鎖的時間長度
     */
    private static final int LOCK_TIME = 10;
    /**
     * access_token 的過期時間
     */
    private static final int LOCK_ACCESS_TOKEN_TIME = 60;
    /**
     * access_token_flag 的過期時間
     */
    private static final int LOCK_ACCESS_TOKEN_FLAG_TIME = 30;
    /**
     * 分布式鎖的時間單位
     */
    private static final TimeUnit LOCK_TIME_SECONDS = TimeUnit.SECONDS;
    private static final TimeUnit LOCK_TIME_MINUTES = TimeUnit.MINUTES;
    /**
     * 日志
     */
    private static final Log log = new Log(GetAccessTokenServiceImpl.class);

    /**
     * 分布式鎖的key
     */
    private static final String LOCK_KEY = "LOCK_KEY";
    /**
     * 緩存中 access_token  的 key
     */
    private static final String ACCESS_TOKEN = "access_token";

    /**
     * 緩存中 access_token_flag 的 key
     */
    private static final String ACCESS_TOKEN_FLAG = "access_token_flag";

    @Autowired
    ThirdCacheServiceImpl thirdCacheServiceImpl;

    @Autowired
    private RedissonClient redissonClient;


    /**
     * 從緩存獲取access_token氓鄙,如果緩存中不存在則從微信服務(wù)獲取access_token
     *
     * @param trace   流水號
     * @param busCode 業(yè)務(wù)碼
     * @return ResultData
     */
    @Override
    public ResultData<?> getAccessToken(String trace, String busCode) {
        Long startTime = System.currentTimeMillis();
        log.reqPrint(Log.THIRDPARTY_SIGN, Log.THIRDPARTY_REQUEST, trace, busCode, "");
        ResultData<?> resultData;
        try {
            // 從緩存獲取 ACCESS_TOKEN_FLAG
            resultData = thirdCacheServiceImpl.get(trace, ACCESS_TOKEN);
            // 緩存響應(yīng)失敗的情況
            if (null == resultData || null == resultData.getResultCode()
                    || !ThridPartyResCode._0000.equals(resultData.getResultCode())) {
                log.respPrint(Log.THIRDPARTY_SIGN, Log.THIRDPARTY_RESPONSE, trace,
                        busCode, System.currentTimeMillis() - startTime, JSON.toJSONString(resultData));
                return resultData;
            }
            // 緩存響應(yīng)正常,但access_token過期失效的情況
            if (ThridPartyResCode._0000.equals(resultData.getResultCode()) && null == resultData.getResultData()) {
                //主動調(diào)用业舍,從微信服務(wù)獲取access_token,存到緩存中
                setAccessToken(trace, busCode);
                resultData = thirdCacheServiceImpl.get(trace, ACCESS_TOKEN);
            }
        } catch (Exception e) {
            resultData = new ResultData<>();
            resultData.setResultCode(ThridPartyResCode._0001);
            resultData.setDesc("Get accessToken fail...");
            resultData.setException(e);
            log.error("trace:" + trace + ",busCode:" + busCode + "get [key:" + ACCESS_TOKEN_FLAG + "] fail", e);
        }
        log.respPrint(Log.THIRDPARTY_SIGN, Log.THIRDPARTY_RESPONSE, trace,
                busCode, System.currentTimeMillis() - startTime, JSON.toJSONString(resultData));
        return resultData;
    }

    /**
     * 主動調(diào)用抖拦,從微信服務(wù)獲取access_token,存到緩存中
     *
     * @param trace   流水號
     * @param busCode 接口號
     */
    @Override
    public void setAccessToken(String trace, String busCode) {
        Long startTime = System.currentTimeMillis();
        log.reqPrint(Log.THIRDPARTY_SIGN, Log.THIRDPARTY_REQUEST, trace, busCode, "");
        //從微信服務(wù)獲取access_token
        setAccessToken();
        log.respPrint(Log.THIRDPARTY_SIGN, Log.THIRDPARTY_RESPONSE, trace,
                busCode, System.currentTimeMillis() - startTime, "setAccessToken");
    }

    /**
     * 被動調(diào)用,定時從微信服務(wù)獲取access_token
     */
    @Scheduled(cron = "0 0 0/1 * * * ")
    public void setAccessToken() {
        Long startTime = System.currentTimeMillis();
        RLock lock = null;
        boolean res = false;
        try {
            log.info("update access_token begin...");
            // 獲取分布式鎖
            lock = redissonClient.getFairLock(LOCK_KEY);
            // 嘗試加鎖舷暮,最多等待1秒态罪,加鎖成功后10秒鐘自動釋放鎖
            res = lock.tryLock(5,LOCK_TIME, LOCK_TIME_SECONDS);
            if (false==res){
                log.info("get lock failure...");
                return;
            }
            // 如果ACCESS_TOKEN_FLAG不為空,說明30分鐘內(nèi)已經(jīng)有線程更新過ACCESS_TOKEN下面,此次不用更新ACCESS_TOKEN
            //if (null != redissonClient.getBucket(ACCESS_TOKEN_FLAG).get()
            //        && null != redissonClient.getBucket(ACCESS_TOKEN).get()) {
            if(null != thirdCacheServiceImpl.get("", ACCESS_TOKEN_FLAG).getResultData()
                    && null !=thirdCacheServiceImpl.get("", ACCESS_TOKEN)){
                log.info("access_token already exits. spend time" + (System.currentTimeMillis() - startTime));
                return;
            }
            //從微信服務(wù)獲取accessToken
            String accessToken = GetAccessTokenFromWechatServer();
            // 未成功獲取到accessToken
            if (null == accessToken) {
                return;
            }
            // 成功獲取到accessToken
            // 向緩存中保存access_token复颈,60分鐘有效
            redissonClient.getBucket(ACCESS_TOKEN).set(accessToken, LOCK_ACCESS_TOKEN_TIME, LOCK_TIME_MINUTES);
            log.info("update access_token suecss. access_token:"+ accessToken);
            // 設(shè)置ACCESS_TOKEN_FLAG為一個不為null的值,30分鐘內(nèi)有效
            redissonClient.getBucket(ACCESS_TOKEN_FLAG).set("something not null", LOCK_ACCESS_TOKEN_FLAG_TIME, LOCK_TIME_MINUTES);
            log.info("update access_token finish,spend time" + (System.currentTimeMillis() - startTime));
        } catch (Exception e) {
            log.info("set access_token error. " + e.toString());
        } finally {
            if (lock != null) {
                //釋放鎖
                lock.unlock();
            }
        }
    }

    /**
     * 從微信服務(wù)獲取 access_token
     * @return String access_token
     */
    private static String GetAccessTokenFromWechatServer() {
        HttpUriRequest httpUriRequest = RequestBuilder.get()
                .setUri("https://api.weixin.qq.com" + "/cgi-bin/token")
                .addParameter("grant_type", "client_credential")
                .addParameter("appid", "*******************")      //用自己的appid
                .addParameter("secret", "**********************") //用自己的secret
                .build();
        AccessToken accessToken = LocalHttpClient.executeJsonResult(httpUriRequest, AccessToken.class);
        return accessToken.getAccess_token();
    }
}

寫代碼遇到的坑

1 注意 分布式鎖的key 不要與 緩存中 access_token的key 共用沥割。否則會報錯 WRONGTYPE Operation against a key holding the wrong kind of value耗啦,查了一下是redis類型沖突凿菩,比如redis中保存的為String,如果用hash去獲取就會報這個錯帜讲。但是在反復(fù)檢查代碼后確認(rèn)沒有用錯類型衅谷。后來才意識到Redisson分布式鎖Rlock的底層原理其實是用的redis的SetNx,會在redis創(chuàng)建一個key舒帮,Redisson中的Rlock默認(rèn)是用的hash会喝。而如果我們與access_token共用key的話其實就是String與hash的類型沖突了。關(guān)于這一點在Redisson的github中有國外的網(wǎng)友指出了這點玩郊,詳見以下這篇帖子:
https://github.com/redisson/redisson/issues/480肢执。

該帖其中提到了:
Name conflict with different object could cause this bug too. RLock uses Redis map structure in internals, so if you're trying to use lock with name matches with different object you'll get the same error. Here is the code snippet causes this problem:

        String key = String.valueOf("425011000000151");
        redisson.getBucket(key).set("123");

        RLock lc = redisson.getLock(key); 
        lc.lock(1000,TimeUnit.MILLISECONDS);

以下是貼心的英翻中:
不同object的name沖突也可以導(dǎo)致這個bug。Redisson的分布式鎖Rlock底層使用的Redis的map類型來保存它的key译红,所以如果你使用的鎖的名字與你的object沖突的話预茄,就會導(dǎo)致相同的錯誤。上面就是導(dǎo)致這個問題的代碼片段侦厚。在代碼中String 的key與Rlock的key相同耻陕,就會導(dǎo)致WRONGTYPE Operation against a key holding the wrong kind of value。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刨沦,一起剝皮案震驚了整個濱河市诗宣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌想诅,老刑警劉巖召庞,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異来破,居然都是意外死亡篮灼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門徘禁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诅诱,“玉大人,你說我怎么就攤上這事送朱∧锏矗” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵骤菠,是天一觀的道長它改。 經(jīng)常有香客問我,道長商乎,這世上最難降的妖魔是什么央拖? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上鲜戒,老公的妹妹穿的比我還像新娘专控。我一直安慰自己,他們只是感情好遏餐,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布伦腐。 她就那樣靜靜地躺著,像睡著了一般失都。 火紅的嫁衣襯著肌膚如雪柏蘑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天粹庞,我揣著相機與錄音咳焚,去河邊找鬼。 笑死庞溜,一個胖子當(dāng)著我的面吹牛革半,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播流码,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼又官,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了漫试?” 一聲冷哼從身側(cè)響起六敬,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驾荣,沒想到半個月后觉阅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡秘车,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了劫哼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叮趴。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖权烧,靈堂內(nèi)的尸體忽然破棺而出眯亦,到底是詐尸還是另有隱情,我是刑警寧澤般码,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布妻率,位于F島的核電站,受9級特大地震影響板祝,放射性物質(zhì)發(fā)生泄漏宫静。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望孤里。 院中可真熱鬧伏伯,春花似錦、人聲如沸捌袜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虏等。三九已至弄唧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霍衫,已是汗流浹背候引。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留慕淡,地道東北人背伴。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像峰髓,于是被迫代替她去往敵國和親傻寂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理携兵,服務(wù)發(fā)現(xiàn)疾掰,斷路器,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 微信服務(wù)號開發(fā) 整體流程 域名報備徐紧,服務(wù)器搭建 Python開發(fā)環(huán)境和項目的初始化搭建静檬; 微信公眾號注冊及開發(fā)模式...
    飛行員suke閱讀 4,479評論 0 14
  • 緊合雙目卻比睜眼現(xiàn)實,炯炯圓睜又如蒙閉愚昧并级,或許拂檩,就是文人和其他人的區(qū)別。 人們活在當(dāng)下嘲碧,就好比一場恢宏的即興表演...
    吾欲閱讀 251評論 0 1
  • 常用操作ctrl +z退出當(dāng)前代碼命令#表示單行注釋R語言中沒有多行注釋稻励,多行注釋可通過if(FALSE){}包含...
    周一ing閱讀 1,367評論 0 1