记一次CountDownLatch使用和线上踩坑(QAQ)

记一次CountDownLatch使用和线上踩坑(QAQ)
theme: juejin --- 最近不是在搭建新的系统嘛,我们需要在界面上展示集团的all department,但是在问中台调用部门全量接口的时候发现响应速度比较慢,为了解决这个问题呢使用了CountDownLatch工具类,在这个过程中呢也出现了一些使用上的问题,所以在这里跟大家分享一下~ ## 问题描述 - 先上图集团的组织结构

image.png 通过这个图我们可以看到有三个根结点(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导致阻塞还和线程池的饱和策略有关,具体问题具体分析,欢迎讨论~

分类:
后端