使用多线程+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

@Override
    public 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)
    @Override
    public 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();
            // 设置工具箱id
            importNumRecord.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读取excel
            try {
                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);

                    /**
                     * 执行号码读取
                     */
                    @Override
                    public 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());
                                // 把每条号码记录存入list
                                data.setWorkBoxId(id);
                                data.setCreateTime(LocalDateTime.now());
                                // log.info("读取号码{}", data);
                                cachedDataList.add(data);
                                // 成功号码数量+1
                                successNum.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);
                                }
                            }
                        }
                    }

                    /**
                     * 最后执行
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext context) {
                        // 避免所有数据都已经重复,执行空插入,出现sql错误
                        if (cachedDataList.size() > 0) {
                            // 保存不足5000条的剩余数据
                            saveData();
                            // 清空set
                            cacheDataSet = 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_record
        set
        work_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">
        select
            imr.id,
            imr.work_box_id,
            imr.file_name,
            imr.status,
            imr.result,
            imr.create_time
        from import_num_record imr
        where 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、代码可以分块抽取出来

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值