請求合併的總結

這幾天基本每天都會看視頻跟着學習一些東西,然後總結一下。

今天的總結是關於請求合併。

業務背景:優化接口的處理。預防系統爲線程開銷過大導致OOM。

處理思路:時間換空間的一種處理方式,在方法級別上 將同一時間請求的參數暫時堆積一起,然後合併成一個批量請求到數據庫或者下游業務上。將線程開銷的壓力阻塞在上游服務上。

應用場景:部分業務在高併發的情況下會因爲系統線程開銷出現問題,但是專門爲這個業務去增設服務器後,業務平峯的情況下服務器完全閒置產生性能浪費。出現的一種解決方案。

優缺點:優點我理解就是處理性能上的提升,系統開銷的減少。缺點:我理解多次請求合併成一個。查詢功能還好,

如果是複雜的業務,有一個失敗導致的異常,會導致別的參數也失敗。那麼需要使用者對接口的業務實現做到知根知底。

 

首先我們利用countdownlatch模擬一下1000個線程併發問題的業務場景。

或者我們可以利用測試的一些併發工具去做這些事情。

然後我們去看調用的requestSumService.getStr方法。

public String getStr(int id)throws Exception ;

很簡單的一個根據id返回String的一個接口。裏面的實現暫定返回 return id+"test";

正常的業務場景下,我們將在系統啓動後,啓動1000個線程進行併發訪問。每個返回耗時基本在2-3毫秒。

如果查數據庫的話,我們的數據庫就要同時創建1000個線程進行處理。

這個時候我們在方法內部做一些改變。

思路:我們的方法新增一個全局的無界隊列(LinkedBlockingQueue),在收到請求後,不進行處理,而是將參數封裝成一個對象Request,然後將這個對象放入隊列。

同時,我們在類的中間新增一個方法,加上@PostConstruct,內部實現一個定時器,每20ms從隊列中取出收到的請求數據,將請求數據作爲參數,調用一個批量處理的下游業務處理方法。從而實現合併請求。

上代碼:

@Service
public class RequetSumService implements IRequestSumService {

    /**
     * 無界阻塞隊列 用來存儲臨時保存的參數信息  先進先出
     *
     */
    private LinkedBlockingQueue<Request> queue = new LinkedBlockingQueue<>();

    /**
     * 參數傳遞和返回的中間介質
     */
    class Request{
        /**
         * 請求參數信息
         */
        public int id;
        /**
         * 線程future信息 分發返回信息用 @since 1.8
         */
        public CompletableFuture<String> future;
    }

    /**
     * 利用類初始加載時執行一次的特性 進行一個定時器的加載
     * 定時的業務:每20毫秒掃描一次隊列的數據,取出請求參數 改成批量查詢數據庫或者第三方
     * 空間換時間
     */
    @PostConstruct
    public void timerMethod() {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(()->{
            int size = queue.size();
            if (size > 0) {
                List<Request> requestList = Lists.newArrayList();
                log.info("請求一次,本次請求數量-{}", size);
                for(int i = 0 ;i < size;i++){
                    requestList.add(queue.poll());
                }
                // 批量請求合併成一個請求去處理數據
                List<Integer> idList = Lists.newArrayList();
                for(Request request : requestList){
                    idList.add(request.id);
                }
                Map<Integer, String> resultMap = getStrByIdList(idList);
                // 收到集體返回的信息後 進行分發 這裏涉及到多線程中間的通信和傳值問題 這裏用到futrue
                for(Request request : requestList){
                    request.future.complete(resultMap.get(request.id));
                }
            }
        },0,20, TimeUnit.MILLISECONDS);
    }

    /**
     * 單次請求的方法
     * 思路:將請求的數據 以數據的形式 放到類初始的隊列中 然後利用futrue值傳遞帶回來值
     *      在特定的業務場景下有用處
     * @param id
     * @return
     */
    public String getStr(int id) throws Exception {
        CompletableFuture<String> future = new CompletableFuture<>();
        Request request = new Request();
        request.id = id;
        request.future = future;
        //將參數合併 裝入隊列中 等待定時器合併執行
        queue.add(request);
        return future.get();
    }

    /**
     * 模擬數據庫和其他業務方的批量獲取方法 這裏寫成本地方法 沒有判空和去重等校驗
     * @param idList
     * @return
     */
    public Map<Integer, String> getStrByIdList(List<Integer> idList) {
        Map<Integer, String> resultMap = Maps.newHashMap();
        for (int id : idList) {
            resultMap.put(id, id + "test");
        }
        return resultMap;
    }
}

批量調用數據返回的是衆多請求參數的一個結果集,那麼如果來區分哪個請求對應哪個結果呢。

這裏用到的是future.   ->CompletableFuture. @since1.8

默認我們Thread繼承的是Runable,返回值是void,是達不到我們的要求的。

CompletableFuture可以異步等待返回值。上面我們在獲取到結果集後,根據封裝參數裏帶的future可以來設置value。

然後通過future.get()來獲取值。

擴展:get()和join()都可以實現獲取異步返回值。

兩者的區別一個是報錯的異常不一樣,一個是get()是有等待時間的。兩者在線程沒有獲取到值前,

都會調用wait(boolean flag)來獲取值,flag爲是否可以中斷。get爲true,false爲false.  get()方法是會主動中斷的。

要學習的地方還很多。知道越多才越發現自己的無知。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章