背景:當前系統A需調用第三方服務接口接口,因查詢併發量太大,致使第三方接口不穩定,java
爲了減小查詢請求量, 建議優化如下兩點:node
A系統線上服務是集羣的,有16臺服務器,因此爲了知足以上兩點需求,則可轉化爲如下兩點:redis
解決方案:緩存
public static ConcurrentMap<String, CacheResult> cacheMap = new ConcurrentHashMap<>() if (cacheMap.containsKey(cacheKey)) { return getResultByRam(cacheKey, query); } else { // 建立CacheResult,放入本地緩存cacheMap CacheResult cacheResult = new CacheResult(cacheKey); boolean lockFlag = getZkLock(cacheKey, cacheResult, query); cacheMap.put(cacheKey, cacheResult); String minChildPath = zkUtil.getMinChildPath(cacheKey); cacheResult.setZkPath(minChildPath); if (lockFlag) return doWorkAfterLockSuccess(query, cacheKey, minChildPath); return doWorkAfterLockFailed(cacheKey, minChildPath, query); } ================== private FlightQueryResult getResultByRam(String cacheKey, FlightQueryParam query) throws Exception { FlightQueryResult result; String msg = ""; CacheResult cacheResult = cacheMap.get(cacheKey); log.info("cacheResult.getStatus():{}", cacheResult.getStatus()); switch (cacheResult.getStatus()) { case INIT: LockUtil.await(cacheKey); cacheResult = cacheMap.get(cacheKey); msg = "來自內存數據 - 有等待"; break; case VALID: msg = "來自內存數據 - 無等待"; default: break; } result.setMessage(cacheResult.getMsg() + msg + result.getTransId()); return result; } ================== @Data public class CacheResult{ private String cacheKey; private String zkPath; // 該key對應的zk node path private CacheStatus status = CacheStatus.INIT; private FlightQueryResult result; private String msg = ""; public long CacheTimeStamp = System.currentTimeMillis();//當前時間戳 public CacheResult(String cacheKey) { this.cacheKey = cacheKey; } public void setMsg(String msg) { if (StringUtils.isNotBlank(msg)) { this.msg = msg + " || "; } } } ================== public enum CacheStatus { INIT("INIT", "初始化狀態", 1), VALID("VALID", "有效狀態", 2), EXPIRE("EXPIRE", "過時", 3); }
1.將請求入參轉化爲惟一cacheKey,判斷本地緩存中是否有該cacheKey;服務器
2.當本地緩存不存在時,經過zookeeper獲取分佈式鎖,利用序列化節點特性實現樂觀鎖的方式保證只有一個請求獲取分佈式鎖成功,相同參數建立相同目錄,但由於是序列化節點,zookeeper會保證時序性,xxx_node0001,xxx_node0002;當是最小當值時表示獲取到了分佈式鎖;併發
1.監聽本身建立的節點 2.調用第三方服務接口,返回結果 3.啓動十分鐘定時任務,保證數據十分鐘保鮮期 4.將返回的結果放入redis中 5.將zookeeper節點狀態值改成有效狀態(VALID);
1.刪除本身建立的節點,並監聽序列化值最小的節點 2.獲取最小節點目錄的值進行判斷 init:調用await方法,讓當前線程等待 valid:從cacheMap中獲取result結果,並返回
當獲取到分佈式鎖以後,zookeeper節點狀態值改成VALID時,全部監聽最小節點的服務器都會獲取事件,從redis中獲取數據,並將數據放入當前服務器的本地內存中,最後調用lockUtil.signAll方法;分佈式
當十分鐘後定時任務觸發,會將zk中的狀態改成Expired值,這時監聽最小節點的服務器都會收穫expired事件;這時只須要刪除本地緩存便可;優化
這種設計方式有如下幾個好處:this
1.防止大量的請求訪問redis,減輕redis壓力線程
2.redis雖然有expire功能,可是沒有事件觸發,每一個服務器仍是須要去查詢redis,形成壓力;同時責任單一,redis只負責數據存儲
當獲取樂觀鎖的服務器down機:
全部監聽最小節點當服務器會接收節點刪除事件,一樣會刪除本地內存;不會影響業務;
以上解決方案遵循兩點:
1.經過concurrentHashMap實現本地服務器只有一個請求有機會調用第三方服務接口
2.經過zk樂觀鎖機制,保證多臺服務器只有一臺服務器的一個請求調用第三方服務接口 redis只負責存儲數據,zk保證分佈式鎖的惟一獲取