记一次CountDownLatch使用和线上踩坑(QAQ) theme: juejin — 最近不是在搭建新的体系嘛,我们需要在界面上展示集团的all department,但是在问中台调用部分全量接口的时分发现呼应速度比较慢,为了解决这个问题呢使用了CountDownLatch东西类,在这个进程中呢也出现了一些使用上的问题,所以在这儿跟我们分享一下~ ## 问题描述 – 先上图集团的安排结构

记一次CountDownLatch使用和线上踩坑(QAQ)
经过这个图我们能够看到有三个根结点(xx集团实际不存在,前史原由于虚拟跟部分),我们要分别去中台取三次才能够拿到三个部分下的一切部分,之后做一次聚合回来给前端供用户做一些使用。

解决办法:

  • 首要针对上面的问题,第一版就是直接去调三次,做聚合回来
public List<DepartmentInfoDTO> getAllDepartmentList() {
    List<DepartmentInfoDTO> departmentList = Lists.newArrayList();
     // rootDepartmentNumber是三个部分的 departmentId配置
    for (String departmentNumber : UtilString.splitToStringList(rootDepartmentNumber)) {
        DepartmentSearchListParam param = new DepartmentSearchListParam();
        param.setNumber(departmentNumber);
        List<DepartmentResponseDTO> response = departmentApi.globalSearch(param).getData();
        if (null == response) {
            continue;
        }
        departmentList.addAll(transDepDTOs(response));
    }
    return departmentList;
}

之后发现了问题,由于是同步去调,接口耗时很长(16000个部分),经常会超时,导致前端加载不出来之后在优化进程中思考的时分发现这个场景刚好符合countDownLatch的概念,我们先回忆一下:

countDownLatch概念

  1. CountDownLatch是一个同步东西类,用来和谐多个线程之间的同步,或者说起到线程之间的通讯(而不是用作互斥的作用),能够使一个线程在等候别的一些线程完结各自作业之后,再持续履行。使用一个计数器进行完结。计数器初始值为线程的数量。当每一个线程完结自己使命后,计数器的值就会减一。当计数器的值为0时,表明一切的线程都现已完结一些使命,然后在CountDownLatch上等候的线程就能够康复履行接下来的使命 综上所述,结合当时的状况,我能够让主线程做聚合的作业,在三个查询线程完结之前主线程一向等候,直到计数器为0,这时分唤醒主线程去做聚合 回来

优化之后的代码

public List<DepartmentInfoDTO> getAllDepartmentList() {
    List<DepartmentInfoDTO> departmentList = Collections.synchronizedList(Lists.newArrayList());
    List<String> rootNumbers = UtilString.splitToStringList(rootDepartmentNumber);
    CountDownLatch countDownLatch = new CountDownLatch(rootNumbers.size());
    for (String departmentNumber : rootNumbers) {
        // 使用配置的自定义线程池去履行使命
        threadPollExecutor.execute(() -> {
            try {
                DepartmentSearchListParam param = new DepartmentSearchListParam();
                param.setNumber(departmentNumber);
                List<DepartmentResponseDTO> response = departmentApi.globalSearch(param).getData();
                if (null == response) {
                    return;
                }
                departmentList.addAll(transDepDTOs(response));
                countDownLatch.countDown();
            }catch (Exception e) {
                log.error("获取全量部分失败,请查看:{}", e.getMessage(), e);
            }
        });
    }
    try {
        countDownLatch.await();
    }catch (InterruptedException ie) {
        log.warn("async get department has been interrupt");
        // 设置线程的中止标志,产生中止捕获后要让更高级别的中止处理程序会注意到它而且能够正确处理它
        Thread.currentThread().interrupt();
    }
    return departmentList;
}

之后结合countDownLatch做了优化,果然速度提升了超级多,也没有出现过接口呼应超时的状况,能够依照预期完结使用,自测、测验成果也正常,本来以为这样没问题了,在上线运行一段时间之后产生了大问题。中台接口回来数据格式产生了错误导致transDepDTOs方法里反常,countDown没有履行,导致线程一向在堵塞,最终线程池打满,体系一向在报警,且服务不可用,在机器上下载堆栈日志后发现,都是线程堵塞,之后剖析到了上面的代码里(剖析进程参阅fullGc排查,差不多进程),发现countDownLatch.countDown()不触发导致的,最终紧迫fix把问题修复之后好了,修复的代码如下:

public List<DepartmentInfoDTO> getAllDepartmentList() {
    List<DepartmentInfoDTO> departmentList = Collections.synchronizedList(Lists.newArrayList());
    List<String> rootNumbers = UtilString.splitToStringList(rootDepartmentNumber);
    CountDownLatch countDownLatch = new CountDownLatch(rootNumbers.size());
    for (String departmentNumber : rootNumbers) {
        threadPollExecutor.execute(() -> {
            try {
                DepartmentSearchListParam param = new DepartmentSearchListParam();
                param.setNumber(departmentNumber);
                List<DepartmentResponseDTO> response = departmentApi.globalSearch(param).getData();
                if (null == response) {
                    return;
                }
                departmentList.addAll(transDepDTOs(response));
            }catch (Exception e) {
                log.error("获取全量部分失败,请查看:{}", e.getMessage(), e);
            }finally {
                countDownLatch.countDown();
            }
        });
    }
    try {
        countDownLatch.await();
    }catch (InterruptedException ie) {
        log.warn("async get department has been interrupt");
        Thread.currentThread().interrupt();
    }
    return departmentList;
}

由于我这个场景单一而且不能影响其他的范围,接受反常状况也允许countDown,所以在finally里修改了一下代码
今天就到这儿,我们在使用进程中能够结合自己场景去剖析,由于除了countDown导致堵塞还和线程池的饱和策略有关,具体问题具体剖析,欢迎评论~