使用多线程+easyexcel实现异步号码导入
使用多线程+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实现异步号码导入相关推荐
- 使用easyexcel进行excel的导入和导出(web)
使用easyexcel进行excel的导入和导出(web) 前言:使用springboot,mybatis,excel3.x.x,通用mapper.本文主要演示怎么使用easyexcel,因此先展示效 ...
- 过程 线 多线程 并发 同步异步
过程 线 多线程 并发 同步异步 好多人的过程,线,多线程,并发,同步,异步概念混淆,这不是一个好大学讲学的缘故啊.在这里,我们感受到的概念,帮助学生感受审查困惑. 计划 用来描述个别功能程序中描述的 ...
- 3D引擎多线程:资源异步加载
本文原创版权归 博客园 flagship 所有,如有转载,请详细标明作者及原文出处,以示尊重! 作者:flagship 原文:3D引擎多线程:资源异步加载 资源异步加载恐怕是3D引擎中应用最为广泛的多 ...
- 使用EasyExcel实现Excel的导入、导出、下载模板等功能
文章目录 导入功能 依赖 实体类 监听器 控制器 批量插入 导出功能 下载模板 实体类 控制层 业务层 参考:https://blog.csdn.net/z845910508/article/deta ...
- Android中的多线程编程与异步处理
Android中的多线程编程与异步处理 引言 在移动应用开发中,用户体验是至关重要的.一个流畅.高效的应用能够吸引用户并提升用户满意度.然而,移动应用面临着处理复杂业务逻辑.响应用户输入.处理网络请求 ...
- 响应式编程(反应式编程)的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)
响应式编程的来龙去脉(同步编程.多线程编程.异步编程再到响应式编程) 文章目录 响应式编程的来龙去脉(同步编程.多线程编程.异步编程再到响应式编程) 简介 1. 示例 2. 同步编程 3. 多线程编程 ...
- 使用EasyExcel实现Excel的导入导出
文章目录 前言 一.EasyExcel是什么? 二.使用步骤 1.导入依赖 2.编写文件上传配置 3.配置表头对应实体类 4.监听器编写 5.控制层 6.前端代码 总结 前言 在真实的开发者场景中,经 ...
- Android多线程和常用异步处理技术
Android多线程和常用异步处理技术 一.Android多线程概述 1.概述:表示一个程序的多段语句同时执行,但并不等于多次启动一个程序,操作系统也不会把每个线程当作独立的进程来对待. 2.线程和进 ...
- EasyExcel实现Excel文件导入导出
1 EasyExcel简介 EasyExcel是一个基于Java的简单.省内存的读写Excel的开源项目.在尽可能节约内存的情况下支持读写百M的Excel. github地址: https://git ...
最新文章
- windows server 2012 用sysdba登录报错 ORA-01031
- Canvas3 汉化QA和BUG反馈
- [转]#pragma once和#pragma comment
- 如何清除word复制过来的文字背景色的好方法?
- tensorflow.python.framework.tensor_shape.is_fully_defined()
- 编译错误:vulkan/vulkan.h:没有那个文件或目录
- 华为NP课程笔记15-Eth-Trunk与高级VLAN
- 由VB的KeyCode和KeyAscii到扫描码、虚拟码的思考
- NumPy下载与安装
- 聚焦智能制造 香洲区产学研资对接合作活动 盈致科技成功牵手北理珠
- 权限控制 JSR-250注解、@Secured注解、支持表达式的注解
- RxJava 学习笔记(七) --- Filtering 过滤操作
- Python——伪随机数生成器
- 币小秘:如何才能减少被套,降低风险!
- 罗克韦尔AB PLC RSLogix5000中的比较指令使用方法介绍
- AE怎么制作流体文字效果?这波教程我真学会了
- 第4篇 Fast AI深度学习课程——深度学习在回归预测、NLP等领域的应用
- cad怎么画坐标系箭头_cad怎么插入箭头?cad插入箭头方法
- Unity UGUI NGUI 模型 粒子特效 三者之间 渲染层级设置
- AI英雄 | 八问机器学习泰斗Jordan教授:AI其实并不神奇