管理系统必备技(6): Java 大数据量树形目录生成

管理系统必备技(6): Java 大数据量树形目录生成

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

前言

在用 easyexcel 导入各种数据,并生成对应的关系入库之后,接下来的问题就是查找出它们的目录关系。而这个过程就产生了意外,所以出现了这篇文章来记录这个开发过程。

一、背景

某个电站的层级数据如图,这些目录关系是存放在一个关系目录中的,所以这个目录表具备的数据在 36w 条左右。

之前的思路是,找出所有的 list 数据 ,然后找出父目录节点,在把父目录节点和list 数组传入,逐层遍历,代码如下,但是面对大数据目录的时候简直束手无策,采用这种递归效果执行的时间是无法想象的,会出现几十分钟的执行过程。五层递归,以及36w数据找目标值。时间复杂度 相当于36 w 的5次方。约等于循环168w亿次。

    private List<WiringPointVO> treePoint(List<WiringPointVO> wiringPointVOList) {
        List<WiringPointVO> wiringPointVOS = new ArrayList<>();
        for (int i = 0; i < wiringPointVOList.size(); i++) {
            WiringPointVO wiringPointVO = wiringPointVOList.get(i);
            if (0 != wiringPointVO.getParentId().intValue()) {
                continue;
            }
            WiringPointVO wiringPointVO1 = new WiringPointVO();
            BeanUtil.copyProperties(wiringPointVO, wiringPointVO1);
            wiringPointVO1.setChild(subPoint(wiringPointVO, wiringPointVOList));
            wiringPointVOS.add(wiringPointVO1);
        }
        return wiringPointVOS;
    }
复制代码
 private List<WiringPointVO> subPoint(WiringPointVO parentEquipment, List<WiringPointVO> wiringPointVOList) {
        List<WiringPointVO> list = new ArrayList<>();
        for (int i = 0; i < wiringPointVOList.size(); i++) {
            WiringPointVO wiringPointVO = wiringPointVOList.get(i);
            if (wiringPointVO.getParentId().equals(parentEquipment.getId())) {
                WiringPointVO wiringPointVO1 = new WiringPointVO();
                BeanUtil.copyProperties(wiringPointVO, wiringPointVO1);
                wiringPointVO1.setChild(subPoint(wiringPointVO, wiringPointVOList));
                list.add(wiringPointVO1);
            }
        }
        return list;
    }
复制代码

二、解决思路

在面对这种问题时,首先我们需要判断出在哪一步耗费时间,除了 36w 条数据的sql 优化之外,最关键的就是如何组装大数据量的目录。

递归的出现使得代码更加的简介了,但因为没有对数据进行处理,导致会出现的问题就是需要从 36w 条数据中找几条数据的值,并且反复出现,所以,想到的就是把目录进行分组。根据设备类型,分为箱变、汇流、逆变器、支路、光伏 五组内容。

虽然五组内容包括的是所有进线的数据,而我们只需要查找一条进线的数据,但是这五组内容粒度上是不能再分了。

找出这五组数据后,我们抛弃递归,手动组装目录数据。

步骤如下:

  • 1、根据进线id 找出 61 个箱变中的 A 个箱变,此时child不赋值
  • 2、遍历A 个箱变,从522 个汇流中找出 B 个汇流,此时child不赋值
  • 3、遍历 B 个汇流,从2014 个逆变器中找出 C 个逆变器,此时child不赋值
  • 4、遍历 C个逆变器,从15579 个支路中找出 D 个支路,此时child不赋值
  • 5、遍历 D个支路,从342738 个光伏中找出 E 个光伏,然后对D 的child 分别赋值找到的对应光伏list
  • 6、组装逆变,遍历 C 个逆变器,从D个支路中找出 parentId = C 的id 的值然后给C 的child 赋值
  • 7、组装汇流,遍历 B 个汇流,从C 个逆变器中找出 parentId = B的id 的值然后给 B 的child 赋值
  • 8、组装箱变,遍历 A 个箱变,从B 个汇流中找出 parentId =A的id 的值然后给A 的child 赋值
  • 9、返回最后组装的箱变list,赋值给进线的child即完成目录的组装

其中步骤5 是最耗费时间的,需要循环 D * 342738 次。而D 的值估摸为 2000个 需要进线约 70亿次循环。而这步也已经浓缩到不能最小了,所以这步的循环可以采用多线程来处理。

三、具体代码

3.1 业务代码

 private WiringPointVO subPoint2(WiringPointVO parentEquipment, List<WiringPointVO> wiringPointVOList) {
        // 4万目录的查询优化,递归效率太低了。
        // 分为五大小组
        List<WiringPointVO> xbBig = new ArrayList<>();
        List<WiringPointVO> hlBig = new ArrayList<>();
        List<WiringPointVO> nbBig = new ArrayList<>();
        List<WiringPointVO> zlBig = new ArrayList<>();
        List<WiringPointVO> gfBig = new ArrayList<>();
​
        List<WiringPointVO> xbList = new ArrayList<>(20);
        List<WiringPointVO> hlList = new ArrayList<>(100);
        List<WiringPointVO> nbList = new ArrayList<>(500);
        List<WiringPointVO> zlList = new ArrayList<>(5000);
​
        Map<String, List<WiringPointVO>> collect = wiringPointVOList.stream().filter(i->i.getType()!=null).collect(Collectors.groupingBy(WiringPoint::getType));
        for (String s : collect.keySet()) {
            if(s.equals(WmConst.DEV_TYPE_CONST_1.getName())){ xbBig=collect.get(s); }
            if(s.equals(WmConst.DEV_TYPE_CONST_2.getName())){ hlBig=collect.get(s); }
            if(s.equals(WmConst.DEV_TYPE_CONST_3.getName())){ nbBig=collect.get(s); }
            if(s.equals(WmConst.DEV_TYPE_CONST_4.getName())){ zlBig=collect.get(s); }
            if(s.equals(WmConst.DEV_TYPE_CONST_5.getName())){ gfBig=collect.get(s); }
        }
​
        Long  start = System.currentTimeMillis();
        // 1、先组装箱变,此时箱变的 child 为null
        xbBig.forEach(i->{
            if(i.getParentId().equals(parentEquipment.getId())){
                xbList.add(i);
            }
        });
        log.info("箱变加载结束,耗时{}...",System.currentTimeMillis()-start);
        // 2、组装汇流箱,,此时汇流的 child 为 null
        for (WiringPointVO xb : xbList) {
            hlBig.forEach(i->{
                if(i.getParentId().equals(xb.getId())){
                    hlList.add(i);
                }
            });
        }
        log.info("汇流箱加载结束..耗时{}...",System.currentTimeMillis()-start);
        // 3、组装逆变器,此时逆变器的 child 为null
        for (WiringPointVO hl : hlList) {
            nbBig.forEach(i->{
                if(i.getParentId().equals(hl.getId())){
                    nbList.add(i);
                }
            });
        }
        log.info("逆变器加载结束..耗时{}...",System.currentTimeMillis()-start);
        // 4、组装支路
        for (WiringPointVO nb : nbList) {
            zlBig.forEach(i->{
                if(i.getParentId().equals(nb.getId())){
                    zlList.add(i);
                }
            });
        }
        log.info("支路加载结束..耗时{}...",System.currentTimeMillis()-start);
        // 5、组装光伏
        for (WiringPointVO zl : zlList) {
            List<WiringPointVO> finalGfBig = gfBig;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    ArrayList<WiringPointVO> wiringPointVOS = new ArrayList<>();
                    finalGfBig.forEach(i->{
                        if(i.getParentId().equals(zl.getId())){
                            wiringPointVOS.add(i);
                        }
                    });
                    zl.setChild(wiringPointVOS);
                }
            });
        }
        log.info("光伏加载结束..耗时{}...",System.currentTimeMillis()-start);
​
        // 1、组装逆变
        Map<Integer, List<WiringPointVO>> zlmap = zlList.stream().collect(Collectors.groupingBy(WiringPoint::getParentId));
        nbList.forEach(i->{ if(ObjectUtil.isNotNull(zlmap.get(i.getId()))){
            i.setChild(zlmap.get(i.getId()));
        } });
        log.info("组装逆变耗时时间:{}...",System.currentTimeMillis()-start);
        // 2、组装汇流
        Map<Integer, List<WiringPointVO>> nbmap = nbList.stream().collect(Collectors.groupingBy(WiringPoint::getParentId));
        hlList.forEach(i->{ if(ObjectUtil.isNotNull(nbmap.get(i.getId()))){
            i.setChild(nbmap.get(i.getId()));
        } });
        log.info("组装汇流耗时时间:{}...",System.currentTimeMillis()-start);
        // 3、组装箱变
        Map<Integer, List<WiringPointVO>> hlmap = hlList.stream().collect(Collectors.groupingBy(WiringPoint::getParentId));
        xbList.forEach(i->{ if(ObjectUtil.isNotNull(hlmap.get(i.getId()))){
            i.setChild(hlmap.get(i.getId()));
        } });
        log.info("最后耗时:{}...",System.currentTimeMillis()-start);
        parentEquipment.setChild(xbList);
        return parentEquipment;
    }
复制代码

3.2 线程池

/**
 * @author xiao lei
 * @Date 2020/11/6
 * @module
 */public class CustomThreadPoolExecutor {
    private ThreadPoolExecutor poolExecutor = null;
    /**
     * 线程池初始化方法
     * <p>
     * corePoolSize 核心线程池大小----1
     * maximumPoolSize 最大线程池大小----3
     * keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间----30+单位TimeUnit
     * TimeUnit keepAliveTime时间单位----TimeUnit.MINUTES
     * workQueue 阻塞队列----new ArrayBlockingQueue<Runnable>(5)====5容量的阻塞队列
     * threadFactory 新建线程工厂----new CustomThreadFactory()====定制的线程工厂
     * rejectedExecutionHandler 当提交任务数超过maxmumPoolSize+workQueue之和时,
     * 即当提交第41个任务时(前面线程都没有执行完,此测试方法中用sleep(100)),
     * 任务会交给RejectedExecutionHandler来处理
     */
    public void init() {
        poolExecutor = new ThreadPoolExecutor(
                7,
                7,
                30,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(300),
                new CustomThreadFactory(),
                new CustomRejectedExecutionHandler());
    }
​
    public void destory() {
        if (poolExecutor != null) {
            poolExecutor.shutdownNow();
        }
    }
​
​
    public ExecutorService getCustomThreadPoolExecutor() {
        return this.poolExecutor;
    }
​
    private class CustomThreadFactory implements ThreadFactory {
​
        private AtomicInteger count = new AtomicInteger(0);
​
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            String threadName = CustomThreadPoolExecutor.class.getSimpleName() + count.addAndGet(1);
            System.out.println(threadName);
            t.setName(threadName);
            return t;
        }
    }
​
​
    private class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
​
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                // 核心改造点,由blockingqueue的offer改成put阻塞方法
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码
public class ThreadPoolConfig {
    public static ExecutorService initPool() {
        CustomThreadPoolExecutor exec = new CustomThreadPoolExecutor();
        // 初始化
        exec.init();
        ExecutorService pool = exec.getCustomThreadPoolExecutor();
        return pool;
    }
}
复制代码

线程池核心线程得根据电脑的核数来设定,其中的 CPU 核数又分为 IO型和计算型。

本问题中的是计算型。一般 IO 型的线程设为核数的两倍。而计算型的线程数设为核数-1 比较合适。

从原先代码的几十分钟,到单线程的20多秒,以及多线程的4-5秒的时间。此时单纯从数据库中查询已经优化到了一定程度,还未加上redis 的缓存。

以上就是本问题的一种解决方法,

分类:
后端
标签: