高并发处理:请求合并
场景:在高并发的查询场景下,如果查询的参数都是相似的,类似id=1,id=2,id=3这种那么就可以通过请求合并来解决
请求合并就是每隔一段时间就将这段时间内的请求合并到一起进行批量查询,减少查询数据库的操作。
请求合并是以时间换空间的方式
类似于Redis就是以空间换时间的方式
技术实现要求:
LinkedBlockQueue阻塞队列
ScheduledThreadPoolExecutor 定时任务线程池
CompleteableFuture future阻塞机制
@Service public class CommodityService { @Autowired QueryServiceRemoteCall queryServiceRemoteCall; /** * 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分 * CompletableFuture将处理结果返回 */ class Request{ String code; CompletableFuture completableFuture; } /* LinkedBlockingQueue是一个阻塞的队列,内部采用链表的结果,通过两个ReenTrantLock来保证线程安全 LinkedBlockingQueue与ArrayBlockingQueue的区别 ArrayBlockingQueue默认指定了长度,而LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,也就是无界队列,在移除的速度小于添加的速度时,容易造成OOM。 ArrayBlockingQueue的存储容器是数组,而LinkedBlockingQueue是存储容器是链表 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 */ LinkedBlockingQueue<Request> queue = new LinkedBlockingQueue(); //这里因为是测试,所以使用的是无界队列 @PostConstruct public void init(){ //定时任务线程池,创建一个支持定时、周期性或延时任务的限定线程数目(这里传入的是1)的线程池 //scheduleAtFixedRate是周期性执行 schedule是延迟执行 initialDelay是初始延迟 period是周期间隔 后面是单位 //这里我写的是周期性执行10毫秒执行一次 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(()->{ int size = queue.size(); //如果队列没数据,表示这段时间没有请求,直接返回 if(size==0){ return; } List<Request> list = new ArrayList<>(); System.out.println("合并了"+size+"个请求"); //将队列的请求消费到一个集合保存 for (int i=0;i<size;i++){ list.add(queue.poll()); } //拿到我们需要去数据库查询的特征,保存为集合 List<String> commodityCodes = new ArrayList<>(); for (Request request : list) { commodityCodes.add(request.code); } //将参数传入service处理 Map<String, HashMap<String, Object>> response = queryServiceRemoteCall.queryCommodityByCodeBatch(commodityCodes); //将处理结果返回各自的请求 for (Request request : list) { Map<String,Object> result = response.get(request.code); request.completableFuture.complete(result); //completableFuture.complete方法完成赋值,这一步执行完毕,阻塞的请求可以继续执行了 } },0,10,TimeUnit.MILLISECONDS); } public Map<String,Object> queryCommodity(String code) throws ExecutionException, InterruptedException { Request request = new Request(); request.code = code; CompletableFuture<Map<String,Object>> future = new CompletableFuture<>(); request.completableFuture = future; //将对象传入队列 queue.add(request); //如果这时候没完成赋值,那么就会阻塞,知道能够拿到值 return future.get(); }
@SpringBootTest public class RequestMerge { @Autowired CommodityService commodityService; @Test void context() throws ExecutionException, InterruptedException { //CountDownLatch来让主线程等待 CountDownLatch countDownLatch = new CountDownLatch(100); for (int i=0;i<100;i++){ final String code = "code"+i; Thread thread = new Thread(() -> { try { Map<String, Object> map = commodityService.queryCommodity("000" + code); System.out.println(Thread.currentThread().getName() + "的查询结果是:" + map); } catch (Exception e) { System.out.println(Thread.currentThread().getName() + "出现异常:" + e.getMessage()); e.printStackTrace(); } countDownLatch.countDown(); }); thread.setName("price-thread-"+code); thread.start(); } countDownLatch.await(); } }
@Service public class QueryServiceRemoteCall { /** * 模拟从数据库查询 * @param codes * @return */ public Map<String,HashMap<String, Object>> queryCommodityByCodeBatch(List<String> codes) { Map<String,HashMap<String, Object>> result = new HashMap(); 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.put(code,hashMap); } return result; } }
缺点:请求的时间在执行实际的逻辑之前增加了等待时间,不适合低并发的场景。
优点:可以将每段时间内的请求合并,减少数据库的压力,避免大量重复语句的查询。