這幾天基本每天都會看視頻跟着學習一些東西,然後總結一下。
今天的總結是關於請求合併。
業務背景:優化接口的處理。預防系統爲線程開銷過大導致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()方法是會主動中斷的。
要學習的地方還很多。知道越多才越發現自己的無知。