项目场景:

在系统中有一个通过上传表格进行业务数据导入的操作,使用了线程池和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;
    }

}