高併發(一)

背景:當前系統A需調用第三方服務接口接口,因查詢併發量太大,致使第三方接口不穩定,java

爲了減小查詢請求量, 建議優化如下兩點:node

  • 一樣參數的請求十分鐘內只請求一次
  • 一樣參數返回的結果保鮮十分鐘

A系統線上服務是集羣的,有16臺服務器,因此爲了知足以上兩點需求,則可轉化爲如下兩點:redis

  • 相同入參的請求十分鐘內16臺服務器請求第三方服務共一次
  • 相同入參請求返回的結果十分鐘內需在16臺服務器中共享;

解決方案:緩存

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;服務器

  • 當存在時,獲取相對應的CacheResult對象,CacheResult.status是初始化狀態時,則當前線程進入waiting狀態;有效狀態時,則直接返回CacheResult.result;
  • 當不存在時,建立CacheResult對象,CacheResult.status默認是初始化狀態

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保證分佈式鎖的惟一獲取

相關文章
相關標籤/搜索