同时处理多请求(带超时时间)

在上一篇 同时处理多个请求,记录了同时处理多个请求的几种方式,本篇主要介绍多线程处理时,进行超时控制。也就是说超时了的任务扔掉,未超时的任务返回

在研究线程相关的API时,发现了future.get(timeout, unit)方法,意思是在指定的时间内会等待任务执行,超时则抛异常。激动之余赶紧试了下:

修改下ParallelService中的接口(三个请求的用时分别是1s、2s、10s):

@Slf4j
@Service
public class ParallelService {

    public String requestA() {
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            log.info("requestA被打断");
        }
        return "A";
    }

    public String requestB() {
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            log.info("requestB被打断");
        }
        return "B";
    }

    public String requestC() {
        try {
            TimeUnit.MILLISECONDS.sleep(10000);
        } catch (InterruptedException e) {
            log.info("requestC被打断");
        }
        return "C";
    }

}

增加测试方法(超时时间是3s,也就是说超过3s的任务会被扔掉):

   /**
     * 多线程请求(带超时时间)
     */
    @GetMapping("/test4")
    public void test4() {
        long start = System.currentTimeMillis();

        List<String> list = new ArrayList<>();
        List<Future<String>> futureList = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
        IntStream.range(0, 3).forEach(index -> {
            Future<String> task = executor.submit(() -> request(index));
            futureList.add(task);
        });
        for (int i = 0; i < futureList.size(); i++) {
            Future<String> future = futureList.get(i);
            try {
                // future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。
                String result = future.get(3, TimeUnit.SECONDS);
                log.info("结果:{}", result);
                list.add(result);
            } catch (TimeoutException e) {
                log.info("task{},超时", i);
                // 强制取消该任务
                future.cancel(true);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
        // 停止接收新任务,原来的任务继续执行
        executor.shutdown();

        log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
    }

发送请求,结果:

表面上看感觉效果达到了,时长超过3s的请求被扔掉了。但仔细看发现了个问题:响应时长为啥是5s左右,不应该是3s吗?

在future.get方法前后加上日志:

long start1 = System.currentTimeMillis();
// future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。
String result = future.get(3, TimeUnit.SECONDS);
log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1);
list.add(result);

重新请求一次,结果:

好奇怪,为啥requestB的用时是1s呢?通过多次试验终于发现了真相:

future.get(timeout, unit):在指定的时间内会等待任务执行,超时则抛异常。任务执行的时间是获取到结果的时长。由于每个任务是同时执行的, 但是获取结果时,是阻塞的,也就是串行获取的,所以每个任务获取结果的时长 = 当前任务请求时长 - 上一个任务请求时长。

由此可以计算出任务a时长是1s,任务b是2-1=1s,任务c是10-2=8s。至于总时长5s = 任务b获取结果用时2s + 超时时间3s

结论:当只有一个任务时,超时时间有效,当多个任务执行时,超时时间无效

 

那如果将future.get(timeout, unit) 方法放在一个子线程中,异步去获取结果,能达到效果吗?拭目以待:

   /**
     * 多线程请求(带超时时间)
     */
    @GetMapping("/test5")
    public void test5() {
        long start = System.currentTimeMillis();

        List<String> list = new ArrayList<>();
        List<Future<String>> futureList = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
        IntStream.range(0, 3).forEach(index -> {
            Future<String> task = executor.submit(() -> request(index));
            futureList.add(task);
        });
        for (int i = 0; i < futureList.size(); i++) {
            Future<String> future = futureList.get(i);
            ExecutorService executor2 = Executors.newSingleThreadExecutor();
            int j = i;
            executor2.execute(() -> {
                try {
                    // 在指定的时间内会等待任务执行,超时则抛异常
                    long start1 = System.currentTimeMillis();
                    String result = future.get(2, TimeUnit.SECONDS);
                    log.info("结果:{},用时:{}", result, System.currentTimeMillis() - start1);
                    list.add(result);
                } catch (TimeoutException e) {
                    log.info("task{},超时", j);
                    future.cancel(true);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            });
        }

        // 停止接收新任务,原来的任务继续执行
        executor.shutdown();

        while (true) {
            // 将future.get放入子线程后,由于不会阻塞,所以就直接运行到下面。需要通过判断所有线程是否结束来获取最终结果
            if (executor.isTerminated()) {
                log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
                break;
            }
        }
    }

请求一次,结果:

 完美达到我们的效果!再来压测一下:

从JMeter结果可以看到:平均响应时长:3438ms,最小响应时长:2415ms,最大响应时长:4707ms,TPS:8.1/sec

结论:虽然代码复杂一点,但是效果基本达到了,不过有一点,开启的线程的翻了一倍,对内存消耗比较大

 

再次研究api,找到了最终的大招,使用invokeAll(tasks, timeout, unit)方法:

   /**
     * 多线程请求(带超时时间)
     */
    @GetMapping("/test6")
    public void test6() {
        long start = System.currentTimeMillis();

        List<String> list = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(3); // 开启3个线程
        List<Callable<String>> callableList = new ArrayList<>();
        IntStream.range(0, 3).forEach(index -> {
            callableList.add(() -> request(index));
        });
        try {
            log.info("开始执行");
            long start1 = System.currentTimeMillis();
            // invokeAll会阻塞。必须等待所有的任务执行完成后统一返回,这里的超时时间是针对的所有tasks,而不是单个task的超时时间。
            // 如果超时,会取消没有执行完的所有任务,并抛出超时异常
            List<Future<String>> futureList = executor.invokeAll(callableList, 2, TimeUnit.SECONDS);
            log.info("执行完,用时:{}", System.currentTimeMillis() - start1);
            for (int i = 0; i < futureList.size(); i++) {
                Future<String> future = futureList.get(i);
                try {
                    list.add(future.get());
                } catch (CancellationException e) {
                    log.info("超时任务:{}", i);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
        } catch (InterruptedException e1) {
            log.info("线程被中断");
        }

        // 停止接收新任务,原来的任务继续执行
        executor.shutdown();

        log.info("多线程,响应结果:{},响应时长:{}", Arrays.toString(list.toArray()), System.currentTimeMillis() - start);
    }

请求一次,结果:

完美,太完美了,再来压测一下:

从JMeter结果可以看到:平均响应时长:2114ms,最小响应时长:2007ms,最大响应时长:2485ms,TPS:13.3/sec

结论:代码很简洁,效率也很好

 

posted @ 2019-08-08 14:19 仅此而已-远方 阅读(...) 评论(...) 编辑 收藏