项目场景:
在系统中有一个通过上传表格进行业务数据导入的操作,使用了线程池和countDownLatch来处理以提升效率。简单讲一下怎么用。
描述
Service代码如下
/**
* 特殊车辆 文件导入数据库
*
* @param file
*/
@Override
@SneakyThrows
public void importExcel(MultipartFile file) {
List<SpecialVehicleImportExcelDto> ts = EasyExcelUtil.syncReadModel(FileUtils.convertMultipartFileToFile(file), SpecialVehicleImportExcelDto.class, 0, 1);
List<SpecialVehicleDto> res = new CopyOnWriteArrayList<>();
List<SpecialVehicleDto> lose = new CopyOnWriteArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(ts.size());
for (SpecialVehicleImportExcelDto next : ts) {
//
executor.execute(() -> {
SpecialVehicleDto entity = new SpecialVehicleDto();
BeanUtils.copyProperties(next, entity);
ParkingLotEntity parkingLotEntity = parkingLotDao.selectOne(new LambdaQueryWrapper<ParkingLotEntity>().eq(ParkingLotEntity::getParkingLotName, next.getParkingLotName()));
if (parkingLotEntity == null) {
log.warn(next.getParkingLotName() + "*************** 停车场不存在 ***************");
lose.add(entity);
} else {
entity.setPid(parkingLotEntity.getId().toString());
res.add(entity);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
if (!lose.isEmpty()) {
log.error("*************** 停车场不存在,添加失败 ***************");
throw new RRException(lose.stream().map(SpecialVehicleDto::getParkingLotName).collect(Collectors.toList()) + "停车场不存在,添加失败");
}
transactionTemplate.execute(status -> {
for (SpecialVehicleDto re : res) {
saveData(re);
}
return null;
});
log.info("*************** 特殊车excel导入完成 ***************");
}
这段代码的功能是从Excel文件中读取特殊车辆信息,然后根据停车场名称查询停车场信息,将特殊车辆信息保存到数据库中。如果停车场不存在,则记录下来并抛出异常。
步骤解释:
1. 通过EasyExcelUtil工具类读取Excel文件中的特殊车辆信息,并转换为SpecialVehicleImportExcelDto对象的列表。
2. 创建两个CopyOnWriteArrayList类型的列表res和lose,用于存储特殊车辆信息和未找到停车场的特殊车辆信息。
3. 创建一个CountDownLatch对象,用于控制线程的执行。
4. 遍历特殊车辆信息列表,对每个特殊车辆信息进行处理:
- 使用executor执行异步任务,将特殊车辆信息转换为SpecialVehicleDto对象。
- 根据停车场名称查询停车场信息,如果停车场不存在,则将特殊车辆信息添加到lose列表中;否则将停车场ID设置到特殊车辆信息中,并将特殊车辆信息添加到res列表中。
- 调用countDownLatch的countDown方法,表示当前任务执行完成。
5. 谷歌countDownLatch的await方法,等待所有任务执行完成。
6. 如果lose列表不为空,则记录日志并抛出异常,提示停车场不存在。
7. 使用transactionTemplate执行数据库事务操作,将res列表中的特殊车辆信息保存到数据库中。 8. 记录导入完成的日志信息。
技术分析:
CountDownLatch的使用
在代码中,把Excel文件转换成DTOList后,创建了一个以List的大小为参数的CountDownLatch变量
CountDownLatch countDownLatch = new CountDownLatch(ts.size());
遍历该List,使用线程池进行对业务队列中元素的操作,由于每个元素都要进行数据库操作,所以这里使用了多线程来提高效率。每次循环执行完之后,进行countDown操作,计数减一。
for (SpecialVehicleImportExcelDto next : ts) {
//
executor.execute(() -> {
SpecialVehicleDto entity = new SpecialVehicleDto();
BeanUtils.copyProperties(next, entity);
ParkingLotEntity parkingLotEntity = parkingLotDao.selectOne(new LambdaQueryWrapper<ParkingLotEntity>().eq(ParkingLotEntity::getParkingLotName, next.getParkingLotName()));
if (parkingLotEntity == null) {
log.warn(next.getParkingLotName() + "*************** 停车场不存在 ***************");
lose.add(entity);
} else {
entity.setPid(parkingLotEntity.getId().toString());
res.add(entity);
}
countDownLatch.countDown();
});
}
未执行完多线程任务的时候,使用await方法停止线程继续往下走
countDownLatch.await();
这里简单说一下这两个方法是做什么的
- CountDownLatch.CountDown()
是一个Java中的方法调用,用于减少CountDownLatch对象的计数器值。在多线程编程中,当一个线程完成了它的任务后,可以通过调用 countDown()
方法来减少CountDownLatch的计数器值,表示一个任务已经完成。当计数器值减为0时,所有线程都可以继续执行。
- CountDownLatch.await()
`countDownLatch.await();` 是一个Java中的方法调用,用于阻塞当前线程,直到CountDownLatch对象的计数器值减为0为止。在多线程编程中,当一个或多个线程调用CountDownLatch的 `await()` 方法时,它们会被阻塞,直到所有线程都完成其任务并调用 `countDown()` 方法,使得CountDownLatch的计数器值减为0,才会继续执行。这个方法通常用于协调多个线程之间的执行顺序。
事务的使用
以上所有步骤执行完之后,开始使用事务方法写入数据库,其实这里有一些要优化的点,比如说事务等。这个后续再表。
if (!lose.isEmpty()) {
log.error("*************** 停车场不存在,添加失败 ***************");
throw new RRException(lose.stream().map(SpecialVehicleDto::getParkingLotName).collect(Collectors.toList()) + "停车场不存在,添加失败");
}
transactionTemplate.execute(status -> {
for (SpecialVehicleDto re : res) {
saveData(re);
}
return null;
});
log.info("*************** 特殊车excel导入完成 ***************");
优化方案:
可以在线程池中使用事务,既保证了效率又可以防止出意外,但是这种做法只回滚当前线程的事务,不会回滚全部的事务
executor.execute(() -> {
transactionTemplate.execute(
new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
userMapper.insertBatch(list);
int m = 1 / 0;
List<Integer> ids = list.stream().map(User::getId).collect(Collectors.toList());
log.info("新增成功用户成功,主键为{},当前线程为{}", ids, Thread.currentThread().getName());
log.info("我执行了{}", finalI);
}
}
);
});
其他用到的代码:
线程池设置
@Configuration
@EnableAsync
@Slf4j
public class ExecutorConfig {
@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(300);
executor.setThreadNamePrefix("asyncServiceExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}