通常我們的系統查詢過程如下圖
當我們面臨百萬級甚至更過查詢請求時,這種簡單應用結構明顯無法滿足業務及性能要求。此時我們需要對上述結構進行優化。通常的做法有一下兩種方式
- 負載均衡
將應用後臺集羣部署,降低單個後臺服務器壓力。 - 緩存、緩衝
服務層查詢數據庫時做緩存(Cache/Redis)、緩衝(MQ)
但是上述兩種優化方式中百萬級別的請求依然會進行百萬級別的數據庫或者緩存訪問。並沒有降低服務層服務器、緩存服務器、數據庫服務器壓力。服務器又無法無限擴張,此時可以通過請求合併的方式進行優化。請求合併是將前端大量的相同內容的請求做一次中轉合併,降低服務層以及數據庫查詢的壓力。
優化前
每次前端請求都查詢一次數據庫
try{
countDownLatch.await();
ReqMerger reqMerger =reqMergerService.queryById(id);
System.out.println("第"+id+"次請求,結果:"+reqMerger);
}catch (Exception ex){
ex.printStackTrace();
}
測試
模擬併發1000個請求
static int THREAD_NUM=1000;
static CountDownLatch countDownLatch=new CountDownLatch(THREAD_NUM);
@ResponseBody
@RequestMapping(value="request/single",method ={RequestMethod.GET})
@ApiOperation(value = "測試:單次請求", notes = "測試單次請求", response = Result.class)
public void singleRequest(
HttpServletRequest req,
HttpServletResponse resp){
for(int i=0;i<THREAD_NUM;i++){
final int id=i;
Thread thread=new Thread(()->{
try{
countDownLatch.await();
//每次請求都直接通過service獲取數據
ReqMerger reqMerger =reqMergerService.queryById(id);
System.out.println("第"+id+"次請求,結果:"+reqMerger);
}catch (Exception ex){
ex.printStackTrace();
}
});
thread.setName("線程-"+i);
thread.start();
countDownLatch.countDown();
}
}
測試結果
1000次請求則查詢1000次數據並返回結果
第216次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@49714d61
第519次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@1ddf25d
第518次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@537f1e85
第521次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@7bfc1127
第515次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@661bc49d
第514次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@7ce7a566
第511次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@26bdd1d2
第517次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@75744aeb
第516次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@9b9abf7
第513次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@2d6dcc5f
第512次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@1852941e
第509次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@1dc83922
第507次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@5348df6f
第506次請求,結果:com.ym.reqmerger.entity.ReqMergerEntity@42f1262
優化後
添加一層服務,先將請求緩存至隊列中,同時設置定時任務每隔10ms從隊列中獲取緩存的請求併合並後調用接口方式批量查詢數據再返回結果
@Resource
ReqMergerService reqMergerService;
//自定義類用於包裝請求
class Request{
int id;
CompletableFuture<ReqMerger> future;
}
//將前端請求先緩存至隊列中
LinkedBlockingQueue<Request> queue=new LinkedBlockingQueue<>();
/**
* 添加定時任務
* 設置每10ms從緩存隊列中將緩存的請求合併後調用接口進行查詢
*/
@PostConstruct
public void init(){
ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(()->{
synchronized (queue){
int size=queue.size();
if(size==0){
return;
}
ArrayList<Request> requests=new ArrayList<>();
int[] ids=new int[size];
for (int i = 0; i < size; i++) {
Request req=queue.poll();
requests.add(req);
ids[i]=req.id;
}
System.out.println("合併了"+size+"個請求");
List<ReqMerger> reqMergers=reqMergerService.queryByIds(ids);
Map<Integer,ReqMerger> responseMap=new HashMap<>();
for (ReqMerger response : reqMergers) {
responseMap.put(response.getId(),response);
}
for (Request request : requests) {
ReqMerger result=responseMap.get(request.id);
request.future.complete(result);
}
}
},0,10, TimeUnit.MILLISECONDS);
}
/**
* 請求過來時先將請求緩存至隊列中
* 通過Future來阻塞線程,等待合併的請求執行完後返回結果
* @param id
* @return
* @throws Exception
*/
public ReqMerger queryById(int id) throws Exception{
Request req=new Request();
req.id=id;
CompletableFuture<ReqMerger> future=new CompletableFuture<>();
req.future=future;
queue.add(req);
return future.get();
}
測試
模擬併發1000個請求
@ResponseBody
@RequestMapping(value="request/merger",method ={RequestMethod.GET})
@ApiOperation(value = "測試:合併請求", notes = "測試合併請求", response = Result.class)
public void mergerRequest(
HttpServletRequest req,
HttpServletResponse resp){
for(int i=0;i<THREAD_NUM;i++){
final int id=i;
Thread thread=new Thread(()->{
try{
countDownLatch.await();
//每次請求都直接通過service獲取數據
ReqMerger reqMerger =reqMergerCall.queryById(id);
System.out.println("第"+id+"次請求,結果:"+reqMerger);
}catch (Exception ex){
ex.printStackTrace();
}
});
thread.setName("線程-"+i);
thread.start();
countDownLatch.countDown();
}
}
測試結果
合併了23個請求
合併了176個請求
合併了434個請求
合併了9個請求
合併了301個請求
合併了57個請求
結論
通過對比發現同樣併發1000個請求,未優化時會調用1000次ReqMergerService中的查詢方法,優化後僅僅調用了6次。在高併發情況下大大降低了服務層、數據庫層壓力。相應的確定也很明顯,每一個前端請求都會進行0-50ms的請求等待,降低了相應速度。