这是我参与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 的缓存。
以上就是本问题的一种解决方法,