高并发场景中,调用批量接口相比调用非批量接口有更大的性能优势。但有时候,请求更多的是单个接口,不能够直接调用批量接口,如果这个接口是高频接口,对其做请求合并就很有必要了。比如电影网站的获取电影详情接口,APP的一次请求是单个接口调用,用户量少的时候请求也不多,完全没问题;但同一时刻往往有大量用户访问电影详情,是个高并发的高频接口,如果都是单次查询,后台就不一定能hold住了。为了优化这个接口,后台可以将相同的请求进行合并,然后调用批量的查询接口。如下图所示
或者下图
设计决绝方案
代码实现
@Test public void benchmark() throws IOException { // 创建 并不是马上发起请求 for (int i = 0; i < THREAD_NUM; i++) { final String code = "code-" + (i + 1); // 番号 // 多线程模拟用户查询请求 Thread thread = new Thread(() -> { try { // 代码在这里等待,等待countDownLatch为0,代表所有线程都start,再运行后续的代码 countDownLatch.await(); // http请求,实际上就是多线程调用这个方法 Map<String, Object> result = commodityService.queryCommodity(code); System.out.println(Thread.currentThread().getName() + " 查询结束,结果是:" + result); } catch (Exception e) { System.out.println(Thread.currentThread().getName() + " 线程执行出现异常:" + e.getMessage()); } }); thread.setName("price-thread-" + code); thread.start(); // 田径。启动后,倒计时器倒计数 减一,代表又有一个线程就绪了 countDownLatch.countDown(); } // 输入任意内容退出 System.in.read(); }
@Service public class CommodityService { class Request { String commdityCode; CompletableFuture<Map<String, Object>> future; // 接受结果 } // 积攒 请求。(每隔N毫秒批量处理一次) LinkedBlockingQueue<Request> queue = new LinkedBlockingQueue<>(); // 定时任务的实现,N秒钟处理一次数据 @PostConstruct public void init() { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(() -> { // 1、 取出queue的请求,生成一次批量查询 int size = queue.size(); if (size == 0) { return; } ArrayList<Request> requests = new ArrayList<>(); for (int i = 0; i < size; i++) { Request request = queue.poll(); requests.add(request); } System.out.println("批量处理数据量:" + size); // 2、 组装一个批量查询(一定需要 目的资源能够支持批量查询。 http) ArrayList<String> commodityCodes = new ArrayList<>(); for (Request request : requests) { commodityCodes.add(request.commdityCode); } List<Map<String, Object>> responses = queryServiceRemoteCall.queryCommodityByCodeBatch(commodityCodes); // 3、将结果响应 分发给每一个单独的用户请求。 由定时任务处理线程 --> 1000个用户的请求线程 // [ // {"code":"500",star: tony} // {"code":"600",star: tony} // ] HashMap<String, Map<String, Object>> responseMap = new HashMap<>(); for (Map<String, Object> response : responses) { String code = response.get("code").toString(); responseMap.put(code, response); } for (Request request : requests) { // 根据请求中携带的能表示唯一参数,去批量查询的结果中找响应 Map<String, Object> result = responseMap.get(request.commdityCode); // 将结果返回到对应的请求线程 request.future.complete(result); } }, 0, 10, TimeUnit.MILLISECONDS); } @Autowired QueryServiceRemoteCall queryServiceRemoteCall; // 1000 用户请求,1000个线程 public Map<String, Object> queryCommodity(String movieCode) throws ExecutionException, InterruptedException { // 1000次 怎么样才能变成 更少的接口 // 思路: 将不同用户的同类请求合并起来 // 并非立刻发起接口调用,请求 收集起来,再进行 Request request = new Request(); request.commdityCode = movieCode; // 异步编程: 获取异步处理的结果 CompletableFuture<Map<String, Object>> future = new CompletableFuture<>(); request.future = future; queue.add(request); return future.get(); // 此处get方法,会阻塞线程运行,直到future有返回 // 什么时候返回结果? 批量查询之后。 怎么进行等待 // return queryServiceRemoteCall.queryMovieInfoByCode(movieCode); } }
@Service public class QueryServiceRemoteCall { /** * 调用远程的商品信息查询接口 * * @param code 商品编码 * @return 返回商品信息,map格式 */ public HashMap<String, Object> queryCommodityByCode(String code) { try { Thread.sleep(50L); } catch (InterruptedException e) { e.printStackTrace(); } HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("commodityId", new Random().nextInt(999999999)); hashMap.put("code", code); hashMap.put("phone", "huawei"); hashMap.put("isOk", "true"); hashMap.put("price","4000"); return hashMap; } /** * 批量查询 - 调用远程的商品信息查询接口 * * @param codes 多个商品编码 * @return 返回多个商品信息 */ public List<Map<String, Object>> queryCommodityByCodeBatch(List<String> codes) { // 不支持批量查询 http://moviewapi.com/query.do?id=10001 --> {code:10001, star:xxxx.....} // http://moviewapi.com/query.do?ids=10001,10002,10003,10004 --> [{code:10001, star///}, {...},{....}] List<Map<String, Object>> result = new ArrayList<>(); for (String code : codes) { HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("commodityId", new Random().nextInt(999999999)); hashMap.put("code", code); hashMap.put("phone", "huawei"); hashMap.put("isOk", "true"); hashMap.put("price","4000"); result.add(hashMap); } return result; } }
弊端:
实行合并请求实际上是执行实际路基之前增加了延迟,如果平均需要5毫秒的执行时间,放在10毫秒做一次批处理的场景下,则最坏的情况可能变成15秒(不适合低延迟的RPC场景、低并发场景)