通常我们的系统查询过程如下图
当我们面临百万级甚至更过查询请求时,这种简单应用结构明显无法满足业务及性能要求。此时我们需要对上述结构进行优化。通常的做法有一下两种方式
- 负载均衡
将应用后台集群部署,降低单个后台服务器压力。 - 缓存、缓冲
服务层查询数据库时做缓存(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的请求等待,降低了相应速度。