业务场景:
今天使用在做项目时,遇到了一个业务场景,其中要求如下:
- 远程调用基础数据服务获取教室树菜单列表
- 远程调用设备服务获取已绑定设备的教室列表
- 通过以上两个列表(树菜单、普通列表)进行处理,自己构造一个具有设备是否绑定标识的新树菜单(并预留拓展功能),这里涉及到递归操作,且需要创建对象
如下图:
设计思路
因为其中涉及到了两个远程服务调用,一个树菜单重新构造(还涉及判断)以及递归操作,所以在这里我选择开启异步线程提高运行效率
代码实现
以下代码为实现类OnlineClassPatrolServiceImpl.java
/**
* 获取教室与设施关系树
* 1、获取已绑定设备的教室ids
* 2、获取当前机构的校区-教学楼-楼层-教室树
* 3、构造新的教室与设备树
*
* @return
* @throws BusException
*/
@Override
public R getClassroomEuqTree() throws BusException {
Long orgId = UserUtil.getUserOrgId();
// 获取已绑定教室ids
List bindClassroomIds = equipmentFeign.getBindClassroomIds(2L, PlatformEnum.PLATFORM_RESOURCE.name());
if (bindClassroomIds == null || bindClassroomIds.size() == 0) {
return R.fail("当前机构未绑定任何设备");
}
// 根据机构获取校区-教学楼-楼层-教室树
List selectTreeByJg = basicDataFeign.getSelectTreeByJg(2L);
if (selectTreeByJg == null || selectTreeByJg.size() == 0) {
return R.fail("当前机构下未设置教室");
}
// 定义教室与设备树以及树高度
List classroomEquTreeVoList = new CopyOnWriteArrayList<>();
// List> futures = new ArrayList<>(selectTreeByJg.size());
// List classroomEquTreeVoList = Collections.synchronizedList(new ArrayList<>());
List classroomEquTreeVoList = new ArrayList<>();
// for (int i = 0; i < selectTreeByJg.size(); i++) {
// int finalI = i;
// futures.add(CompletableFuture.supplyAsync(() -> {
// ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
// temp.setId(selectTreeByJg.get(finalI).getId());
// temp.setLabelName(selectTreeByJg.get(finalI).getLabel());
// temp.setLevel(0);
// temp.setFlag(0);
// if (selectTreeByJg.get(finalI).getChildren() != null && selectTreeByJg.get(finalI).getChildren().size() > 0) {
// temp.setChildList(this.getClassroomEquTreeVoList(selectTreeByJg.get(finalI).getChildren(), 1, bindClassroomIds));
// }
// return temp;
// }, taskExecutor).whenComplete((res,e) -> {
// if (e == null){
// classroomEquTreeVoList.add(finalI,res);
// }
// }));
// }
for (SelectTreeVo vo : selectTreeByJg){
futures.add(CompletableFuture.supplyAsync(() -> {
ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
temp.setId(vo.getId());
temp.setLabelName(vo.getLabel());
temp.setLevel(0);
temp.setFlag(0);
if (vo.getChildren() != null && vo.getChildren().size() > 0) {
temp.setChildList(this.getClassroomEquTreeVoList(vo.getChildren(), 1, bindClassroomIds));
}
return temp;
}, taskExecutor));
}
// 主线程阻塞等待所有异步线程完成任务,并在join返回结果再添加到ArrayList,就不会出现线程安全问题
futures.forEach(future -> classroomEquTreeVoList.add(future.join()));
return R.suc(classroomEquTreeVoList);
}
/**
* 递归获取子集
*
* @param selectTreeByJg 子集
* @param level 层级
* @param bindClassroomIds 已绑定设备的教室ids
* @return
*/
private List getClassroomEquTreeVoList(List selectTreeByJg, int level, List bindClassroomIds) {
if (selectTreeByJg == null || selectTreeByJg.size() == 0) {
return null;
}
List classroomEquTreeVoList = new ArrayList<>(selectTreeByJg.size());
for (SelectTreeVo selectTreeVo : selectTreeByJg) {
ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
temp.setId(selectTreeVo.getId());
temp.setLabelName(selectTreeVo.getLabel());
temp.setLevel(level);
if (selectTreeVo.getChildren() != null && selectTreeVo.getChildren().size() > 0) {
temp.setChildList(this.getClassroomEquTreeVoList(selectTreeVo.getChildren(), level + 1, bindClassroomIds));
}
if (level == 3) {
// 如果是教室
if (bindClassroomIds.contains(selectTreeVo.getId())) {
// 此教室已绑定设备
temp.setFlag(1);
} else {
// 此教室未绑定设备
temp.setFlag(0);
}
} else {
// 校区、教学楼、楼层
temp.setFlag(0);
}
classroomEquTreeVoList.add(temp);
}
return classroomEquTreeVoList;
}
可以看到,我注释的这一块一开始是这样的想法:
我开启异步线程的主要是在进行构造新的自定义树菜单时才开启的,再远程调用基础数据服务和调用设备服务时,我并没有进行异步操作,因为我觉得没必要。
以校区为遍历单位,各校区对的教室树菜单(远程基础数据调用获得)进行递归并判断,当为教室层级时,判断是否存在当前已绑定的教室列表(远程设备服务调用获得),若存在则设置自定义树菜单的flag字段为1(0为绑定,1已绑定)状态。
思路图如下所示:
发现问题
会出现北校区执行比较快的情况,则北校区的数据先放入List中,导致了最好的的结果resultList的顺序与一开始的基础数据的教室树菜单校区顺序不一致情况。
在这里我又不想对resultList结果列表进行重排序,所以我在每次异步任务执行完时,使用whenComplete将当前的List加入最后的classroomEquTreeVoList.add(finalI,res)中(finalI为下标索引,res为当前任务的子结果)
如果此时,北校区执行比较快,当他又是第二个执行,他先进入List,则出现List[1] = 北校区数据。现在下标应该为0的南校区还没进来,北校区由于执行得快,先进来了。
就像下面这个问题:index: 1,Size: 0
出现了线程不安全的问题,导致List classroomEquTreeVoList = 下标出现错误
解决方法(在这里我使用了第三种方法,阻塞主线程方法)
1、使用 CopyOnWriteArrayList (适合需要读多写少的业务场景)
- List
classroomEquTreeVoList = new CopyOnWriteArrayList<>();
2、使用 Collections.synchronizedList 将结果List设置为线程安全类,
- List
classroomEquTreeVoList = Collections.synchronizedList(new ArrayList<>());
3、在主线程进行阻塞等待,当我们的所有异步任务(我这里是各校区完成结果递归)完成时,再将结果添加至新的List:
- futures.forEach(future -> classroomEquTreeVoList.add(future.join()));