1. 程式人生 > >redis+spring 註解Cacheable 設定redis的生存週期。

redis+spring 註解Cacheable 設定redis的生存週期。

業務場景:

1、要取得當日匯率(美元兌人民幣,及人民幣兌美元),精度不高,頻率不高,一天取一到兩次即可。

2、取得的匯率作為所有使用者的基礎匯率用做其它運算。所有使用者共用一套匯率,不區分使用者。

解決方案:

1、初步考慮

     i 、建表,存匯率值。

     ii、用定時任務 呼叫其它網站提供的匯率API 來更新表中固定匯率。頻率用定時任務的引數來設定。

     iii、redis取得表裡的值。設定過期時間。

2、後期優化後考慮

    i、因為頻率不高,可以直接用httpUrlConnection來爬取。代替了別人提供的API介面,(找到的免費匯率API不太滿足方案,要麼只能免費一個月,要麼匯率只有CNF,沒有CNY,要麼就是美元換成歐元),方案可以多設定幾個網站。以防反爬或其它問題取值失敗。

    ii、表也不用建了、定時任務也沒必要,直接用@cacheable設定spring快取時間。過期(一天)後,當第一個使用者呼叫時再爬一次,並快取上。對使用者來說基本上無感。

優缺點:

優的是資源少,速度快,簡單易懂。

缺點是爬取的通病,爬的物件保不準什麼時候就失效,補救措施要多做準備,在這裡用了兩套待爬網站+實在取不到取固定匯率死值的方案。前提是業務要求精度不高。

一、配置很簡單。

<!--database根據自己的業務線來設定-->
    <bean id="sessionJedisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:usePool="true" p:database="0">
        <constructor-arg name="sentinelConfig" ref="redisSentinelConfiguration"/>
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
          p:connectionFactory-ref="sessionJedisConnectionFactory">
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
    </bean>
    <!--快取時長根據自己的業務需求來設定-->
    <!-- 資料庫快取管理器 --><cache:annotation-driven />
    <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg ref="redisTemplate"/>
        <property name="defaultExpiration" value="86400"/>
        <property name="expires">
            <map>
                <entry>
                    <key>
                        <value>exchangeRate</value>
                    </key>
                    <value>86400</value>
                </entry>
            </map>
        </property>
    </bean> 

注意紅色部分是必須加的

exchangeRate是你要快取的Cacheable的value

86400是過期時間(一天)。

二、JAVA中註解的使用。

1 controller中因為包含讀快取和爬取的分支,不要寫cacheable標籤,但要判斷是爬還是讀快取。

    /**
     * 匯率取得
     */
    @ApiResponses(value = { @ApiResponse(code = 200, message = "請求成功") })
    @ApiOperation(value = "匯率取得", notes = "匯率取得")
    @ResponseBody
    @RequestMapping(value = "getExchangeRate", method = RequestMethod.POST)
    public InfoResult<Map<String, Object>> getExchangeRate(HttpServletRequest request) throws ParseException {
        /////////////////////////////////////上略
        // 為空重查,不走cache,清除空cache
        Map<String,Object> res = erService.getExchangeRate("fixedValue");
        if (res == null) {
            try {
                res = erService.callExchangeRate();
                erService.clearCache(userId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        ////////////////////////////////////下略
    }

實現類

@Service
public class esServiceImpl implements esService {
    private static Logger logger = LoggerFactory.getLogger(esImpl.class);
    @Resource(name = "redisTemplate")
    private RedisTemplate<String, Map<String, Object>> redisTemplate;
	/**
	 * 匯率取得工具
         * 爬取頁面資訊中的匯率
         * @url https://xxx.com
	 * @return result
	 */
	@Override
        @Cacheable(value="exchangeRate")
	public Map<String, Object> getExchangeRate(String userId) {
        logger.info("介面getExchangeRate從頁面取得匯率開始...");
        // 預設值
        Map<String, Object> result = null;
        try {
            // 取得匯率
            result = callExchangeRate();
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("匯率取得失敗...");
        }
        logger.info("介面getExchangeRate從頁面取得匯率結束...");
		return result;
	}

    /**
     * 取得匯率 目前只取USD轉CNY的匯率
     *    方案1 獲取來源:從xxx網站取,
     *    方案2 獲取來源:從yyy網站取,
     *    注:如果方案1與2都未取到。則取前端的預設值。
     * @return STRING
     */
    public Map<String, Object> callExchangeRate() throws Exception {
        // get api result
        URL url = new URL("https://xxx");
        StringBuilder sbf = new StringBuilder();
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        InputStream is = conn.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        String strRead = null;
        logger.info("方案1: 從xxx取得匯率。");
        while ((strRead = reader.readLine()) != null) {
            if (strRead.contains("currency_converter_result")) {
                String strD[] = strRead.split("xxx");
                //字串解析
            }
        }
        // TYPE 2 GET FROM yyy
        if (sbf.length() == 0) {
            logger.info("方案1 失敗,採用方案2: 從yyy取得匯率。");
            url = new URL("https://yyy");
            conn = (HttpURLConnection) url.openConnection();
            is = conn.getInputStream();
            reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// 字串解析
        String usd2cnyStr = sbf.toString();
        String cny2usdStr = "";
        if (StringUtils.isNotBlank(usd2cnyStr)) {
            double value = Double.valueOf(usd2cnyStr);
            BigDecimal bd = new BigDecimal(1 /value);
            bd = bd.setScale(4,BigDecimal.ROUND_HALF_UP);
            cny2usdStr = String.valueOf(bd);
        }
        is.close();
        reader.close();
        conn.disconnect();
        logger.info("頁面取得匯率(1美元兌人民幣):"+usd2cnyStr);
        logger.info("頁面取得匯率(1人民幣兌美元):"+cny2usdStr);
        Map<String, Object> redisMap = new HashMap<String, Object>();
        redisMap.put("USD2CNY",usd2cnyStr);
        redisMap.put("CNY2USD",cny2usdStr);
    /**
     * 清除快取
     * @param userId userId
     */
    @Override
    @CacheEvict(value = "exchangeRate")
    public void clearCache(String userId) {
        logger.info("匯率快取已清除");
    }

@Cacheable(value = "exchangeRate")是快取該value

呼叫該方法時,會先從spring cache中取得該KEY中有沒有值,如果有,直接跳過方法取值。如果沒有則呼叫方法。

@CacheEvict(value = "exchangeRate")是清除該value的值。

此處的小技巧是用1除以美元兌人民幣的匯率得到人民幣兌美元,省去了再調爬一次URL。

寫在後面:

@Cacheable標籤有幾點要求:

1、不能用在private上。

2、方案必須有引數、且以 方法名+引數值做為VALUE快取,如果引數不同也不如行。

3、果是用在實現類上,必須用在override方法上,也就是說必須是實現介面的方法,單一個非override的public方法是不可用的。