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方法是不可用的。