请求合并的总结

这几天基本每天都会看视频跟着学习一些东西,然后总结一下。

今天的总结是关于请求合并。

业务背景:优化接口的处理。预防系统为线程开销过大导致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()方法是会主动中断的。

要学习的地方还很多。知道越多才越发现自己的无知。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章