请求的异步处理,多线程处理数据。问题及解决方案整体思路记录

需求描述

用户每周需要上传一个很大的文件,交给后台解析。保存数据。

存在问题,及流程解析
  • 数据处理很慢,在本地跑了半个小时也只处理了百分之10左右。而且同时只存在一次记录(上传第二次时把第一次解析的数据做逻辑删除)
  • 数据处理同一时间只调用一次,上次没有处理完就不重复调用,直接返回上传结果,异步调用数据处理,保存进度数据,执行过程中更改进度
第一次解决思路
  • 得到上传的数据,开启线程调用service层方法,同时service添加 synchronized ,在service层中使用线程池对数据进行异步操作,提升数据处理能力

  • controller层代码(简化)

    public ResultCustom import(MultipartFile file) {
        if (file == null) {
            return ResultCustom.error(500, "导入文件不能为空!");
        }
        String filename = file.getOriginalFilename();
        String type = StrUtil.isBlank(filename) ? "" : filename.substring(filename.lastIndexOf("."));
        if (!StrUtil.equalsAny(type, ".xlsx", ".xls")) {
            return ResultCustom.error(500, "上传文件格式错误!");
        }
		// 解析文件放到service层会出现异常,所以在controller层解析把数据传到service
        InputStream inputStream;
        try {
            inputStream = file.getInputStream();
        } catch (Exception e) {
            return ResultCustom.error(500, e.getMessage());
        }
        ExcelReader reader = ExcelUtil.getReader(inputStream, 0); //指定输入流和sheet

        List<Map<String, Object>> dataMap = reader.readAll();
		// 保存处理进度
        BImprotProgress bImprotProgress = new BImprotProgress();
        bImprotProgress.setFileName(filename); // 文件名
        bImprotProgress.setImportTime(LocalDateTime.now()); // 导入时间
        bImprotProgress.setCreateUser(XXXXXX); // 导入人
        bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_1); // 导入状态
        if (!improtProgressService.save(bImprotProgress)) {
            return ResultCustom.error(500, "处理进度保存失败!");
        }

        // 开后台线程处理导入数据,返回导入状态
        new Thread(() -> XXXService.importy(dataMap, ImprotProgress)).start();

        return ResultCustom.message("数据导入成功,正在处理!");
    }
  • service层代码(简化)
 @Override
    public void importy(List<Map<String, Object>> dataMap, BImprotProgress bImprotProgress) {
       
        // 如果重复上传需要把上次的置为不可用
        LambdaUpdateWrapper<XXXXX> update = Wrappers.lambdaUpdate();
        update.set(XXXX::getIsDelete, BasicsConstant.ISDELETE_1);
        XXXMapper.update(null, update);

        bImprotProgress.setTotalSize(dataMap.size()); // 总数据量
        bImprotProgress.setProgress("0.00%"); // 进度
        bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_2);// 处理中状态
        bImprotProgressMapper.updateById(bImprotProgress);


        // 开10个线程处理数据,加上主线程等待,计数器设置为11
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(11);

        // 定义任务线程池,核心线程为10个,最大线程数为15
        final ExecutorService executorService = ThreadUtil.newExecutor(10, 15);
        // 分页开始,第一页
        AtomicInteger atomicLong = new AtomicInteger(1);
        // 每页数量
        int imit = 100;
        int maxThread = 10;

        LocalDateTime start = LocalDateTime.now(); // 开始时间

        try {
            while (true) {
                int andIncrement = atomicLong.getAndIncrement();
                int basics = (andIncrement - 1) * imit * maxThread;
                
                if ((basics + imit * maxThread) > dataMap.size()) {
                    // 最后一次的数据了
                    handleData(dataMap, basics, dataMap.size(), calendarWeek);
                    break;// 处理完直接跳出
                }
                for (int i = 0; i < maxThread; i++) {
                    int pages = i;
                    executorService.execute(() -> { // 多线程执行
                        handleData(dataMap, basics + pages * imit, basics + (pages + 1) * imit, calendarWeekfinal);
                        try {
                            cyclicBarrier.await(); // 作业完成到达屏障
                        } catch (InterruptedException | BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    });
                }

                // 等待所有子线程到达屏障
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                // 当前处理的数量
                bImprotProgress.setProgress(NumberUtil.mul(NumberUtil.div(imit * maxThread * andIncrement, dataMap.size(), 2), 100) + "%");
                bImprotProgressMapper.updateById(bImprotProgress);
                log.info(StrUtil.format("====> 执行完了第{} 次", andIncrement));
            }
            // 处理成功
            bImprotProgress.setProgress("100%");
            bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_3);
            bImprotProgress.setCompleteTime(LocalDateTime.now());
            bImprotProgressMapper.updateById(bImprotProgress);
        } catch (Exception e) {
            e.printStackTrace();
            bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_4);
            bImprotProgressMapper.updateById(bImprotProgress);
        }

        LocalDateTime end = LocalDateTime.now();// 结束时间
        Duration between = LocalDateTimeUtil.between(start, end);

        executorService.shutdown();

        String sta = LocalDateTimeUtil.format(start, DatePattern.NORM_DATETIME_PATTERN);
        String en = LocalDateTimeUtil.format(end, DatePattern.NORM_DATETIME_PATTERN);

        String format = StrUtil.format("开始时间 : {} ,结束时间 : {} ,执行时间 :{}分钟", sta, en, between.toMinutes());
        log.info(format);
    }

    /**
     * 数据处理
     *
     * @param dataMap 数据集合
     * @param begin   处理开始下标
     * @param end     处理结束下标
     */
    private void handleData(List<Map<String, Object>> dataMap, int begin, int end) {
        String licenseId;
        Integer defnull = 0;
        for (int i = begin; i < end; i++) {
            Map<String, Object> map = dataMap.get(i);
            Set<String> key = map.keySet();
           	// 根据自己业务定义长度集合,避免出现扩容,影响性能
            List<XXX> list = new ArrayList<>(300);
            for (String str : key) {
                // 业务处理
                XXXX
                XXXX
                list.add(XXX);
            }
            saveBatch(list, 50);
            list.clear();
        }
    }
  • 发现的问题,因为数据处理时间会很长,数据上传过程中再次上传应该给失败的反馈,而不是线程等待,一直上传可能会存在很多阻塞状态的线程,而拖垮整体系统
第二次解决思路
  • 在controller层控制调用,而不是在service层中加锁,controller层无法获取锁就说明上次上传的数据在处理中,直接返回失败
	
    private static final Lock LOCK = new ReentrantLock();
    
    public ResultCustom import(MultipartFile file) {
        if (file == null) {
            return ResultCustom.error(500, "导入文件不能为空!");
        }
        String filename = file.getOriginalFilename();
        String type = StrUtil.isBlank(filename) ? "" : filename.substring(filename.lastIndexOf("."));
        if (!StrUtil.equalsAny(type, ".xlsx", ".xls")) {
            return ResultCustom.error(500, "上传文件格式错误!");
        }

        // 是否可以获取锁
        if (!LOCK.tryLock()) {
            return ResultCustom.error(500, "存在正在处理的文件,上传失败!");
        }

		// 解析文件放到service层会出现异常,所以在controller层解析把数据传到service
        InputStream inputStream;
        try {
            inputStream = file.getInputStream();
        } catch (Exception e) {
            LOCK.unlock(); // 解锁
            return ResultCustom.error(500, e.getMessage());
        }
        ExcelReader reader = ExcelUtil.getReader(inputStream, 0); //指定输入流和sheet

        List<Map<String, Object>> dataMap = reader.readAll();
		// 保存处理进度
        BImprotProgress bImprotProgress = new BImprotProgress();
        bImprotProgress.setFileName(filename); // 文件名
        bImprotProgress.setImportTime(LocalDateTime.now()); // 导入时间
        bImprotProgress.setCreateUser(XXXXXX); // 导入人
        bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_1); // 导入状态
        if (!improtProgressService.save(bImprotProgress)) {
        	LOCK.unlock(); // 解锁
            return ResultCustom.error(500, "处理进度保存失败!");
        }
        // 开后台线程处理导入数据,返回导入状态
        new Thread(() -> {
            try {
                ibLaunchStrategyService.importLaunchStrategy(dataMap, bImprotProgress);
            } catch (Exception e) {
                log.error("数据处理失败" + e.getMessage());
                e.printStackTrace();
            } finally {
            	// 解锁
                LOCK.unlock();
            }
        }).start();
        
        return ResultCustom.message("数据导入成功,正在处理!");
    }
  • 按照我这种不太聪明的小脑袋瓜来看,流程是没有什么问题了,可是当我点开了unlock的方法时,看到方法上的注释
    -在这里插入图片描述
  • 我理解的大致意思是,只有获取锁的线程才能执行释放锁,不然就有可能抛 unchecked 异常,早知道我就不点开了,毕竟bug我没发现等于没有。
第三次解决思路
  • 没得办法,上次的处理中解锁操作存在问题,想要在controller中加锁,解锁就必须在异步线程中去做。
  • 方案一:使用Semaphore来让service只允许一个线程访问,设置凭证获取超时时间,超时时返回处理失败,Semaphore大致使用
  • 方案二:参考分布式锁key的实现思路。在系统中定义变量,第一次获取时修改变量内容,下次获取判断变量的值,当数据处理完之后把变量内容改回初始值(我知道这用法很潦草。只是借用了思路,并不是分布式锁的实现逻辑,别喷了,别喷了)
  • 因为我的业务相对明确,所以我选择用第二种方案,做无锁化操作
  • 最终版

    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
    
    public ResultCustom import(MultipartFile file) {
        if (file == null) {
            return ResultCustom.error(500, "导入文件不能为空!");
        }
        String filename = file.getOriginalFilename();
        String type = StrUtil.isBlank(filename) ? "" : filename.substring(filename.lastIndexOf("."));
        if (!StrUtil.equalsAny(type, ".xlsx", ".xls")) {
            return ResultCustom.error(500, "上传文件格式错误!");
        }

        int lock = ATOMIC_INTEGER.getAndIncrement();
        // 是否可以获取锁
        if (lock != 0) {
            return ResultCustom.error(500, "存在正在处理的文件,上传失败!");
        }

		// 解析文件放到service层会出现异常,所以在controller层解析把数据传到service
        InputStream inputStream;
        try {
            inputStream = file.getInputStream();
        } catch (Exception e) {
            // 重置变量,下次上传执行处理操作
            atomicIntegerReset();
            return ResultCustom.error(500, e.getMessage());
        }
        ExcelReader reader = ExcelUtil.getReader(inputStream, 0); //指定输入流和sheet

        List<Map<String, Object>> dataMap = reader.readAll();
		// 保存处理进度
        BImprotProgress bImprotProgress = new BImprotProgress();
        bImprotProgress.setFileName(filename); // 文件名
        bImprotProgress.setImportTime(LocalDateTime.now()); // 导入时间
        bImprotProgress.setCreateUser(XXXXXX); // 导入人
        bImprotProgress.setImportState(BasicsConstant.IMPORT_STATE_1); // 导入状态
        if (!improtProgressService.save(bImprotProgress)) {
            // 重置变量,下次上传执行处理操作
            atomicIntegerReset();
            return ResultCustom.error(500, "处理进度保存失败!");
        }
        // 开后台线程处理导入数据,返回导入状态
        new Thread(() -> {
            try {
                ibLaunchStrategyService.importLaunchStrategy(dataMap, bImprotProgress);
            } catch (Exception e) {
                log.error("数据处理失败" + e.getMessage());
                e.printStackTrace();
            } finally {
            	// 重置变量,下次上传执行处理操作
                atomicIntegerReset();
            }
        }).start();
        
        return ResultCustom.message("数据导入成功,正在处理!");
    }

    // 解锁操作
    private void atomicIntegerReset() {
        int current;
        int next = 0;
        do {
            // 获取原子操作类的值,替换为初始0值
            current = ATOMIC_INTEGER.get();
        } while (!ATOMIC_INTEGER.compareAndSet(current, next));
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值