使用多线程+easyexcel实现异步号码导入

需求

最近项目需要实现excel文件上传批量导入号码

实现

通过多线程+easyexcel的方式实战一手。不多说,上代码,欢迎各位大佬指正。

环境

springboot 2.6.13
mybatis-plus 3.4.2

引入依赖

1. 父pom.xml控制版本

<properties><easyexcel.version>3.1.1</easyexcel.version><commons-io.version>2.7</commons-io.version>
</properties><!-- 引入easyexcel依赖--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${easyexcel.version}</version></dependency><!--commons.io--><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>${commons-io.version}</version></dependency>

2. 子模块

     <!-- 引入easyexcel依赖--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId></dependency>

3 数据库表
号码表

CREATE TABLE `whitelist_numberdtl` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '号码id',`work_box_id` bigint(20) unsigned DEFAULT NULL COMMENT '工具箱id',`number` char(11) DEFAULT NULL COMMENT '号码',`create_time` datetime DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `unq_workbox_num` (`work_box_id`,`number`) USING BTREE COMMENT '工具箱id号码唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=285 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='白名单号码详情表';

导入记录表

CREATE TABLE `import_num_record` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '号码导入记录id',`work_box_id` bigint(20) unsigned DEFAULT NULL COMMENT '工具箱id',`file_name` varchar(255) DEFAULT NULL COMMENT '上传文件名',`status` tinyint(3) unsigned DEFAULT '0' COMMENT '导入状态(0导入中 1导入成功 2导入失败)',`result` varchar(500) DEFAULT NULL COMMENT '导入结果',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`create_by` varchar(64) DEFAULT '' COMMENT '创建者',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`update_by` varchar(64) DEFAULT '' COMMENT '更新者',`del_flag` tinyint(4) DEFAULT NULL COMMENT '逻辑删除标志位',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='号码导入记录表';

3.1 controller接口

/*** 导入号码** @param file 文件* @param id   工具箱id* @param type 工具箱类型* @return*/@SneakyThrows@PostMapping("/importNum")public AjaxResult importNum(MultipartFile file, Long id, Integer type) {// 根据id找出工具箱规则,如果该规则导入状态为1,那么不能继续导入EntWorkBox entWorkBox = entWorkBoxService.getById(id);if (entWorkBox.getImportStatus() == 1) {return AjaxResult.error(BizCodeEnum.IMPORTING_NUMBER.getCode(),BizCodeEnum.IMPORTING_NUMBER.getMsg());} else {// 否则置为1后执行号码导入entWorkBox.setImportStatus(1);entWorkBoxService.updateById(entWorkBox);// 执行号码导入AjaxResult ajaxResult = entWorkBoxService.importNum(file, id, type);return ajaxResult;}}

3.2 service

/*** 导入号码* @param file* @param id* @param type* @return*/AjaxResult importNum(MultipartFile file, Long id, Integer type);

3.3 serviceImpl

@Overridepublic AjaxResult importNum(MultipartFile file, Long id, Integer type) {// 根据类型调用各自的号码导入方法, 0 白名单 1 黑名单 2 空号AjaxResult result = new AjaxResult();switch (type) {case 0:result = whitelistNumberdtlService.importNum(file, id);break;case 1:result = blacklistNumberdtlService.importNum(file, id);break;case 2:result = vacantNumberdtlService.importNum(file, id);break;default:break;}return result;}
/*** 批量导入号码** @param file* @param id* @return*/@SneakyThrows(Exception.class)@Overridepublic AjaxResult importNum(MultipartFile file, Long id) {long start = System.currentTimeMillis();// 启动线程池导入号码ThreadPoolExecutor executor = threadPoolTaskExecutor.getThreadPoolExecutor();CompletableFuture.runAsync(() -> {EntWorkBox entWorkBox = entWorkBoxMapper.selectById(id);// 导入号码结果记录ImportNumRecord importNumRecord = new ImportNumRecord();// 获取文件名String filename = file.getOriginalFilename();// 设置文件名importNumRecord.setFileName(filename);// 组装号码导入结果StringBuilder result = new StringBuilder();// 设置工具箱idimportNumRecord.setWorkBoxId(id);importNumRecord.setStatus(0);importNumRecordMapper.insert(importNumRecord);//成功信息记录StringBuilder successMsg = new StringBuilder();//失败信息记录StringBuilder failureMsg = new StringBuilder();//成功条数AtomicInteger successNum = new AtomicInteger();//失败条数AtomicInteger failureNum = new AtomicInteger();// 查询现有号码集合List<String> nums = whitelistNumberdtlMapper.getNumByWorkBoxId(id);// EasyExcel读取exceltry {EasyExcel.read(file.getInputStream(), WhitelistNumberdtl.class, new ReadListener<WhitelistNumberdtl>() {private int count = 5000;/*** 存储所有非重复号码*/private Set<String> cacheDataSet = new HashSet<>();/*** 临时存储*/private List<WhitelistNumberdtl> cachedDataList = ListUtils.newArrayListWithExpectedSize(count);/*** 执行号码读取*/@Overridepublic void invoke(WhitelistNumberdtl data, AnalysisContext context) {// 如果号码格式错误不予添加if (!data.getNumber().matches(EntWorkBoxConstants.PATTERN_NUM)) {// 失败号码数量+1,超过10个只显示10个,未超过则把错误信息拼接起来failureNum.incrementAndGet();if (failureNum.get() < 10) {String msg = "<br/>" + "手机号 " + data.getNumber() + " 导入失败:号码格式有误!";failureMsg.append(msg);}} else {// 如果号码重复,不予重复添加if (!nums.contains(data.getNumber()) && !cacheDataSet.contains(data.getNumber())) {// set放入首次出现的号码cacheDataSet.add(data.getNumber());// 把每条号码记录存入listdata.setWorkBoxId(id);data.setCreateTime(LocalDateTime.now());// log.info("读取号码{}", data);cachedDataList.add(data);// 成功号码数量+1successNum.incrementAndGet();// 如果list超过2000,执行保存,完成后清空list缓存数据if (cachedDataList.size() >= count) {saveData();cachedDataList = ListUtils.newArrayListWithExpectedSize(count);}} else {// 失败号码数量+1,超过10个只显示10个,未超过则把错误信息拼接起来failureNum.incrementAndGet();if (failureNum.get() < 10) {String msg = "<br/>" + "手机号 " + data.getNumber() + " 导入失败:号码已存在!";failureMsg.append(msg);}}}}/*** 最后执行*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 避免所有数据都已经重复,执行空插入,出现sql错误if (cachedDataList.size() > 0) {// 保存不足5000条的剩余数据saveData();// 清空setcacheDataSet = new HashSet<>();}}/*** 存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", cachedDataList.size());// 批量插入whitelistNumberdtlMapper.insertBatch(cachedDataList);for (WhitelistNumberdtl numberdtl : cachedDataList) {String key = "WHITELIST_"+id+"_"+numberdtl.getNumber();redisCache.setCacheObject(key, numberdtl.getNumber());}log.info("存储数据库/redis成功!");}}).sheet().doRead();// 偏移在其实位置插入记录successMsg.insert(0, "导入成功共 " + successNum + " 条,数据如下:" + "<br/>");if (failureNum.get() > 10) {failureMsg.append("<br/>" + ".....错误号码最多显示10条");}// 偏移在其实位置插入记录failureMsg.insert(0, "导入失败共 " + failureNum + " 条,数据格式不正确,错误如下:");importNumRecord.setResult(successMsg.toString() + failureMsg.toString());importNumRecord.setStatus(1);importNumRecord.setUpdateTime(LocalDateTime.now());importNumRecordMapper.updateByRecord(importNumRecord);} catch (Exception e) {// 异常捕获log.error("导入出错:{}", e, e.getMessage());result.append("导入出错:{}" + e.getMessage());importNumRecord.setStatus(2);importNumRecord.setResult(result.toString());importNumRecordMapper.updateByRecord(importNumRecord);} finally {// 出现异常,恢复导入号码状态   保证恢复导入状态entWorkBox.setImportStatus(0);entWorkBoxMapper.updateById(entWorkBox);}long end = System.currentTimeMillis();log.info("任务执行结束,条数{} 耗时{}", (successNum.get() + failureNum.get()), end - start);}, executor);// 线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}return AjaxResult.success("提交成功");}

3.3 mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.rc.business.mapper.ImportNumRecordMapper"><update id="updateByRecord">update import_num_recordsetwork_box_id=#{workBoxId},file_name=#{fileName},status=#{status},result=#{result},update_time=#{updateTime}where id=#{id}</update><select id="getRecords" resultType="com.rc.business.domain.ImportNumRecord">selectimr.id,imr.work_box_id,imr.file_name,imr.status,imr.result,imr.create_timefrom import_num_record imrwhere imr.work_box_id=#{workBoxId}order by imr.update_time desc</select>
</mapper>

3.4 AjaxResult

import java.util.HashMap;
import java.util.Map;import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.utils.StringUtils;/*** 操作消息提醒* * @author ruoyi*/
public class AjaxResult extends HashMap<String, Object>
{private static final long serialVersionUID = 1L;/** 状态码 */public static final String CODE_TAG = "code";/** 返回内容 */public static final String MSG_TAG = "msg";/** 数据对象 */public static final String result = "result";/*** 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。*/public AjaxResult(){}/*** 初始化一个新创建的 AjaxResult 对象* * @param code 状态码* @param msg 返回内容*/public AjaxResult(int code, String msg){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);}/*** 初始化一个新创建的 AjaxResult 对象* * @param code 状态码* @param msg 返回内容* @param data 数据对象*/public AjaxResult(int code, String msg, Object data){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);if (StringUtils.isNotNull(data)){super.put(result, data);}}/*** 返回成功消息* * @return 成功消息*/public static AjaxResult success(){return AjaxResult.success("操作成功");}/*** 返回成功数据* * @return 成功消息*/public static AjaxResult success(Object data){return AjaxResult.success("操作成功", data);}/*** 返回成功消息* * @param msg 返回内容* @return 成功消息*/public static AjaxResult success(String msg){return AjaxResult.success(msg, null);}/*** 返回成功消息* * @param msg 返回内容* @param data 数据对象* @return 成功消息*/public static AjaxResult success(String msg, Object data){return new AjaxResult(HttpStatus.SUCCESS, msg, data);}/*** 返回错误消息* * @return*/public static AjaxResult error(){return AjaxResult.error("操作失败");}/*** 返回错误消息* * @param msg 返回内容* @return 警告消息*/public static AjaxResult error(String msg){return AjaxResult.error(msg, null);}/*** 返回错误消息* * @param msg 返回内容* @param data 数据对象* @return 警告消息*/public static AjaxResult error(String msg, Object data){return new AjaxResult(HttpStatus.ERROR, msg, data);}/*** 返回错误消息* * @param code 状态码* @param msg 返回内容* @return 警告消息*/public static AjaxResult error(int code, String msg){return new AjaxResult(code, msg, null);}public Object getResult() {return this.get(result);}public Object getCodeTag() {return this.get(CODE_TAG);}
}

后续优化:

1、当前号码校验方式:表数据带字符正则校验+表重复数据校验+库已存在数据校验,当表的量大时,数据校验慢—》测试,无数据导入十万条耗时13秒+,已存在十万条数据继续插入十万条耗时50秒+;
2、代码可以分块抽取出来

使用多线程+easyexcel实现异步号码导入相关推荐

  1. 使用easyexcel进行excel的导入和导出(web)

    使用easyexcel进行excel的导入和导出(web) 前言:使用springboot,mybatis,excel3.x.x,通用mapper.本文主要演示怎么使用easyexcel,因此先展示效 ...

  2. 过程 线 多线程 并发 同步异步

    过程 线 多线程 并发 同步异步 好多人的过程,线,多线程,并发,同步,异步概念混淆,这不是一个好大学讲学的缘故啊.在这里,我们感受到的概念,帮助学生感受审查困惑. 计划 用来描述个别功能程序中描述的 ...

  3. 3D引擎多线程:资源异步加载

    本文原创版权归 博客园 flagship 所有,如有转载,请详细标明作者及原文出处,以示尊重! 作者:flagship 原文:3D引擎多线程:资源异步加载 资源异步加载恐怕是3D引擎中应用最为广泛的多 ...

  4. 使用EasyExcel实现Excel的导入、导出、下载模板等功能

    文章目录 导入功能 依赖 实体类 监听器 控制器 批量插入 导出功能 下载模板 实体类 控制层 业务层 参考:https://blog.csdn.net/z845910508/article/deta ...

  5. Android中的多线程编程与异步处理

    Android中的多线程编程与异步处理 引言 在移动应用开发中,用户体验是至关重要的.一个流畅.高效的应用能够吸引用户并提升用户满意度.然而,移动应用面临着处理复杂业务逻辑.响应用户输入.处理网络请求 ...

  6. 响应式编程(反应式编程)的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)

    响应式编程的来龙去脉(同步编程.多线程编程.异步编程再到响应式编程) 文章目录 响应式编程的来龙去脉(同步编程.多线程编程.异步编程再到响应式编程) 简介 1. 示例 2. 同步编程 3. 多线程编程 ...

  7. 使用EasyExcel实现Excel的导入导出

    文章目录 前言 一.EasyExcel是什么? 二.使用步骤 1.导入依赖 2.编写文件上传配置 3.配置表头对应实体类 4.监听器编写 5.控制层 6.前端代码 总结 前言 在真实的开发者场景中,经 ...

  8. Android多线程和常用异步处理技术

    Android多线程和常用异步处理技术 一.Android多线程概述 1.概述:表示一个程序的多段语句同时执行,但并不等于多次启动一个程序,操作系统也不会把每个线程当作独立的进程来对待. 2.线程和进 ...

  9. EasyExcel实现Excel文件导入导出

    1 EasyExcel简介 EasyExcel是一个基于Java的简单.省内存的读写Excel的开源项目.在尽可能节约内存的情况下支持读写百M的Excel. github地址: https://git ...

最新文章

  1. windows server 2012 用sysdba登录报错 ORA-01031
  2. Canvas3 汉化QA和BUG反馈
  3. [转]#pragma once和#pragma comment
  4. 如何清除word复制过来的文字背景色的好方法?
  5. tensorflow.python.framework.tensor_shape.is_fully_defined()
  6. 编译错误:vulkan/vulkan.h:没有那个文件或目录
  7. 华为NP课程笔记15-Eth-Trunk与高级VLAN
  8. 由VB的KeyCode和KeyAscii到扫描码、虚拟码的思考
  9. NumPy下载与安装
  10. 聚焦智能制造 香洲区产学研资对接合作活动 盈致科技成功牵手北理珠
  11. 权限控制 JSR-250注解、@Secured注解、支持表达式的注解
  12. RxJava 学习笔记(七) --- Filtering 过滤操作
  13. Python——伪随机数生成器
  14. 币小秘:如何才能减少被套,降低风险!
  15. 罗克韦尔AB PLC RSLogix5000中的比较指令使用方法介绍
  16. AE怎么制作流体文字效果?这波教程我真学会了
  17. 第4篇 Fast AI深度学习课程——深度学习在回归预测、NLP等领域的应用
  18. cad怎么画坐标系箭头_cad怎么插入箭头?cad插入箭头方法
  19. Unity UGUI NGUI 模型 粒子特效 三者之间 渲染层级设置
  20. AI英雄 | 八问机器学习泰斗Jordan教授:AI其实并不神奇

热门文章

  1. 软件测试入职第一天,如何快速适应上手?3000字经验总结
  2. mysql 数据库分表三种方法
  3. 12.灰色系统下的灰色预测和灰色分析
  4. APS软件必须满足不同规划要求
  5. 数据结构与算法之解析之路
  6. 360浏览器本地备份文件
  7. 郑州市各区劳动仲裁委员会办公地址及电话
  8. String类的切割功能
  9. ICMAX告诉你除了BGA、SOP,还有那些主流的封装类型?
  10. 蛋白质定位信号预测软件