SpringBoot百万行Excel导入MySQL实践

news2024/9/23 21:30:38

在公司开发时,客户说需要支持大数据量excel导入,所以打算写一篇文章记录下思路和优化过程。

一、前期准备

  1. 首先我们选用的肯定是阿里出品的EasyExcel,对比poi和jxl占内存更少
    easyexcel官方网站
  2. 准备测试的数据库和excel文件,已经和代码一起上传到gitee仓库
    项目代码
  3. 修改mysql的max_allowed_packet
    解决MySQL的PacketTooBigException异常问题
  4. 修改了tomcat上传文件的默认限制,因为文件可能过大,会报错
server:
  port: 8888
  maxHttpHeaderSize: 102400
  servlet:
    context-path: /api
  error:
    include-exception: false
    include-message: always
spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

在这里插入图片描述

  1. 开启MyBatis-Plus的批量插入功能,如果不需要请忽略
    Mybatis-Plus自定义批量插入的实现方法

二、依赖引入

        <!-- easyexcel依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.3.4</version>
        </dependency>

        <!-- hutool依赖 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

        <!-- mysql依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
            <scope>runtime</scope>
        </dependency>

        <!--MyBatis plus配置-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

三、创建所需工具类

  • MultipartFileToFileUtils
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;

/**
 * 文件流工具  将传入的MultipartFile类型转为File类型,Controller接收到的是MultipartFile类型,EasyExcel.read方法所需要的是File类型。
 */
public class MultipartFileUtil {
    private final static String STATIC_PATH = "d:/upload/file/";

    public static File multipartFileToFile(MultipartFile file) throws Exception {
        File toFile = null;
        if (file.getSize() > 0) {
            InputStream ins;
            ins = file.getInputStream();
            toFile = new File(STATIC_PATH+file.getName());
            inputStreamToFile(ins, toFile);
            ins.close();
        }
        return toFile;
    }

    //获取流文件
    private static void inputStreamToFile(InputStream ins, File file) {
        try {
            OutputStream os = Files.newOutputStream(file.toPath());
            int bytesRead;
            byte[] buffer = new byte[8192];
            while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            ins.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

四、创建业务层代码

1.实体类

import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("sys_user")
public class User {

    @ExcelProperty("id")
    private Long id;

    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("身份证号码")
    private String idCard;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("性别")
    private String sex;

    @ExcelProperty("备注")
    private String remark;
}

2. Mapper层

如果不使用MyBatis-Plus的批量插入功能,正常继承BaseMapper就好

import com.demo.config.GemBaseMapper;
import com.demo.eneity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends GemBaseMapper<User> {

}

3. service层

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import com.alibaba.excel.EasyExcel;
import com.demo.eneity.User;
import com.demo.excel.SimpleThreadListener;
import com.demo.utils.MultipartFileUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@AllArgsConstructor
@Slf4j
public class UserService  {
    public String importUserList(MultipartFile file) throws Exception {
        TimeInterval timer = DateUtil.timer();
        //不分片单线程插入
        EasyExcel.read(MultipartFileUtil.multipartFileToFile(file), User.class,new SimpleThreadListener()).sheet().doRead();

        //分片单线程插入
//        EasyExcel.read(MultipartFileUtil.multipartFileToFile(file), User.class,new CutDataListener()).sheet().doRead();

        //多线程
//        EasyExcel.read(MultipartFileUtil.multipartFileToFile(file), User.class,new MultiThreadListener()).sheet().doRead();

        log.info("导入成功,花费时间为{}毫秒", timer.interval());
        return "导入成功";
    }
}

4. controller层

import com.demo.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserService userService;

    @PostMapping("/import")
    public String importUserList(@RequestParam("file") MultipartFile file) throws Exception {
        return userService.importUserList(file);
    }

}

五、创建事件监听器

这里有三个版本的事件监听器,分别为单线程事件监听器、分片事件监听器、多线程事件监听器

1. 单线程事件监听器

import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.demo.eneity.User;
import com.demo.mapper.UserMapper;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Getter
@Component
@Slf4j
public class SimpleThreadListener extends AnalysisEventListener<User> {

    private List<User> list = Collections.synchronizedList(new ArrayList<>());

    public SimpleThreadListener() {

    }

    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        if (user != null) {
            list.add(user);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("解析完毕,开始插入");
        UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
        userMapper.insertBatchSomeColumn(list);
    }
}

2. 分片事件监听器

import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.demo.eneity.User;
import com.demo.mapper.UserMapper;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@Component
@Slf4j
public class CutDataListener extends AnalysisEventListener<User> {

    private List<User> list = new ArrayList<>();

    public CutDataListener() {

    }

    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        if (user != null) {
            list.add(user);
        }
        //分批插入,大于10w执行一次
        if(list.size() >= 100000) {
            saveData();
            list.clear();
        }
    }


    /**
     * 保存数据到db
     */
    private void saveData() {
        UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
        userMapper.insertBatchSomeColumn(list);
        list.clear();
    }


    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        saveData();
        list.clear();
    }
}

3. 多线程事件监听器

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.demo.eneity.User;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;

@Getter
@Component
@Slf4j
public class MultiThreadListener extends AnalysisEventListener<User> {

    private List<User> list = Collections.synchronizedList(new ArrayList<>());
    private final static int CORE_POOL_SIZE = 5;//核心线程数
    private final static int MAX_POOL_SIZE = 10;//最大线程数
    private final static int QUEUE_CAPACITY = 100;//队列大小
    private final static long KEEP_ALIVE_TIME = 1L;//存活时间

    public MultiThreadListener() {

    }

    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        if (user != null) {
            list.add(user);
        }
    }



    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("解析完毕,开始插入新数据");
        //创建一个新的线程池
        ExecutorService executorService = new ThreadPoolExecutor(CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());
        //设置每个线程处理数据的数量
        int singleTreadDealCount = 1000;
        //要提交到线程池的线程数量
        int threadSize = (list.size() / singleTreadDealCount) + 1;
        //开始位置
        int startIndex = 0;
        //结束位置
        int endIndex = 0;
        //初始化闭锁,数量为线程数量
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            //最后一个的结束位置是数组的大小
            if ((i + 1) == threadSize) {
                startIndex = i * singleTreadDealCount;
                endIndex = list.size();
            } else {
                startIndex = i * singleTreadDealCount;
                endIndex = (i + 1) * singleTreadDealCount;
            }
            //创建自定义线程任务类,执行run方法
            UserThread thread = new UserThread(startIndex,endIndex,list,countDownLatch);
            executorService.execute(thread);
        }
        try {
            //当前线程开始等待
            countDownLatch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        //通过countDownLatch控制所有线程都执行完,再关闭线程池
        executorService.shutdown();
        list.clear();
    }
}

4.创建多线程监控器用的线程任务类

import cn.hutool.extra.spring.SpringUtil;
import com.demo.eneity.User;
import com.demo.mapper.UserMapper;

import java.util.List;
import java.util.concurrent.CountDownLatch;


public class UserThread implements Runnable {

    private int startIndex;

    private int endIndex;

    private List<User> list;

    private CountDownLatch count;

    private UserMapper userMapper;

    public UserThread(int startIndex, int endIndex, List<User> list, CountDownLatch count) {
        this.startIndex = startIndex;
        this.endIndex = endIndex;
        this.list = list;
        this.count = count;
    }

    @Override
    public void run() {
        try {
            List<User> newList = list.subList(startIndex, endIndex);
            //防止空插入
            if (newList.size() > 0) {
                UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
                userMapper.insertBatchSomeColumn(newList);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //计数减一
            count.countDown();
        }
    }
}

实现Callable或者Runable或者继承Thread都行,这里实现Runable,重写run方法。然后根据传入位置区间,通过subList方法分割,执行批量插入方法进行数据的入库。在finally中执行coutDown,是为了防止插入时出现异常

注:关于CountDownLatch的详解,可以看我的这篇文章
浅谈CountDownLatch 和 CyclicBarrier

六、测试三种方法的效率

1.单线程

什么操作都不处理的情况下,耗时88秒
在这里插入图片描述
在这里插入图片描述

2.分片单线程

我们在UserService开启分片单线程的方法,然后清空整个数据库
在这里插入图片描述
在这里插入图片描述

重启调用接口,总耗时79秒
在这里插入图片描述
在这里插入图片描述

3.多线程

我们同样在UserService开启多线程的方法,然后清空整个数据库,重启调用接口,总耗时39秒,数据插入也正常
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
速度相对于单线程来说差不多快了一倍的速度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2071316.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Zustand:让React状态管理更简单、更高效

Zustand 这个单词在德语里是状态的意思&#xff08;发音&#xff1a;促stand&#xff09; 1. 下载zustand npm i zustand 或者 yarn add zustand2.创建一个store import { create } from zustandconst useBearStore create((set) > ({bears: 0,increasePopulation: …

20240824给飞凌OK3588-C的核心板刷Ubuntu22.04并连接adb

20240824给飞凌OK3588-C的核心板刷Ubuntu22.04并连接adb 2024/8/24 15:56 缘起&#xff0c;由于我司对面积有极度的追求&#xff0c;所以将飞凌OK3588-C开发板使用的【9线】type-C接口&#xff08;USB3.1?&#xff09;降级为4线的USB2.0。 【micro USB/MINI USB。】 先决条件…

基于RK3588的多摄像头车辆与车道线检测系统(基于rk3588的车辆和车道线检测,可以带四个720p的摄像头,2个1080p的摄像头)

硬件配置&#xff1a; 处理器&#xff1a;Rockchip RK3588&#xff0c;这是一款高性能的嵌入式处理器&#xff0c;支持多路高清视频输入和处理。摄像头配置&#xff1a; 4个720p&#xff08;1280x720&#xff09;分辨率的摄像头2个1080p&#xff08;1920x1080&#xff09;分辨…

什么是持续集成(持续交付、部署)

文章目录 1 持续集成1.1 持续集成的好处1.2 持续集成的目的1.3 没有持续集成的状况 2 持续交付3 持续部署4 持续交付和持续部署的区别 1 持续集成 持续集成&#xff08;Continuous integration&#xff0c;简称CI&#xff09;&#xff0c;简单来说持续集成就是频繁地&#xff…

拓扑排序,以及区间dp相关试题

目录 1.有向无环图(DAG图&#xff09; 2.AOV网:顶点活动图 3.拓扑排序 4.实现拓扑排序 力扣.207课程表 牛客.最长回文子序列 1.有向无环图(DAG图&#xff09; 入度:表示有多少条边指向它 出度:有多少条边向外指出他 2.AOV网:顶点活动图 3.拓扑排序 找到做事情的先后顺序 …

React学习笔记(三)——redux状态管理工具

1. Redux快速上手 1.1 什么是Redux&#xff1f; Redux 是 React 最常用的 集中状态管理工具 &#xff0c;类似于 Vue 中的 Pinia&#xff08;Vuex&#xff09;&#xff0c; 可以独立于框架运行 作用&#xff1a; 通过集中管理的方式管理应用的状态 1.2 Redux快速体验 不和任何…

【OpenGL学习笔记】--图像管线

图像管线&#xff08;Image Pipeline&#xff09;是计算机图形学中一个核心概念&#xff0c;尤其是在图形处理和渲染的上下文中。它是一个用于处理和渲染图像的流程&#xff0c;其中包括从场景数据的输入到最终图像输出的各个阶段。 图像管线的组成 顶点处理&#xff08;Verte…

大模型入门到精通——使用Embedding API及搭建本地知识库(一)

使用Embedding API及搭建本地知识库 1. 基于智谱AI调用Embedding API实现词向量编码 首先&#xff0c;生成.env 文件&#xff0c;填写好智谱AI的API-key 参考&#xff1a;大模型入门到实战——基于智谱API key 调用大模型文本生成 读取本地/项目的环境变量。 find_dotenv(…

基于SSM的在线家教管理系统的设计与实现 (含源码+sql+视频导入教程+论文+PPT)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM的在线家教管理系统拥有三个角色 管理员&#xff1a;用户管理、教师管理、简历管理、申请管理、课程管理、招聘教师管理、应聘管理、评价管理等 教师&#xff1a;课程管理、应聘…

Prometheus Operator部署管理

Prometheus Operator部署管理 Prometheus Operator & Kube-Prometheus & Helm chart 部署区别 Prometheus Operator 是 Kubernetes 原生的工具&#xff0c;它通过将 Prometheus 资源定义为 Kubernetes 对象&#xff08;CRD&#xff09;来简化 Prometheus 集群的管理。…

如何使用ssm实现公司项目管理系统设计与实现

TOC ssm136公司项目管理系统设计与实现jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规…

Avg函数求比率的应用(SQL)

题目 在 SQL 中&#xff0c;AVG 函数用于计算一组数值的平均值。这个功能也可以用来计算比率或比例。 平均值 可以用来计算比率的原因&#xff1a; 二元值&#xff1a;在许多情况下&#xff0c;我们用 1 和 0 表示发生或未发生的事件。例如&#xff0c;在你的查询中&#xff0…

基于Java的C语言课程教学实践小程序的设计与实现(论文+源码)_kaic

基于Java的C语言课程教学实践小程序的设计与实现 摘 要 在当前信息技术迅猛发展的大背景下&#xff0c;为了学生更好地利用信息技术学习C语言&#xff0c;急需开发一款C语言课程教学实践小程序。‎这个小程序可以让学生不再局限于课堂学习‎的教学模式&#xff0c;而是能够随…

freeCAD与stl文件如何互切?

大家好&#xff0c;我是山羊君Goat。 作为硬件工程师&#xff0c;如果需要给自己的硬件主板做一个DIY的造型&#xff0c;比如说B站稚晖君DIY的小电视等等。 对于这个&#xff0c;那3D打印技术就必不可少了&#xff08;怪不得说硬件学的东西都很杂 &#xff09;。 FreeCAD是一款…

BeautifulSoup4通过lxml使用Xpath定位实例

有以下html。<a>中含有图片链接&#xff08;可能有多个<a>&#xff0c;每一个都含有一张图片链接&#xff09;。最后一个<div>中含有文字。 上代码&#xff1a; import requests from bs4 import BeautifulSoup from lxml import etreeurlhttps://www.aaabb…

动态规划类型题目汇总及解析(持续更新)

目录 数字三角形模型 摘花生 最低通行费 方格取数&#xff08;洛谷&#xff09; 传纸条&#xff08;洛谷&#xff09; 最长上升子序列模型 最长上升子序列&#xff08;洛谷&#xff09;&最长递增子序列&#xff08;leetcode&#xff09; leetcode674. 最长连续递…

Image-to-Image Translation 图像翻译任务中的输入成对图像拼接成一张图技术详解

引 言 在图像翻译任务中&#xff0c;近几年比较火热的Generative Adversarial Nets (GAN)模型以及各种变体深受视觉研究团体的青睐&#xff0c;在具体任务中取得不错的实验表现。图像翻译包含两部分内容&#xff1a;一个是图像内容(image content)显示内部存在的实体,用于区分不…

【C++ Primer Plus习题】4.5

问题: 解答: #include <iostream> using namespace std;typedef struct _CandyBar {string brand;float weight;int calorie; }CandyBar;int main() {CandyBar snack { "德芙",2.1,20};cout << "品牌:" << snack.brand << endl;…

计算机毕业设计 养老院管理系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

IO进程day01(函数接口fopen、fclose、fgetc、fputc、fgets、fputs)

目录 函数接口 1》打开文件fopen 2》关闭文件fclose 3》文件读写操作 1> 每次读写一个字符&#xff1a;fgetc(),fputc() 针对文件读写 针对终端读写 练习&#xff1a;实现 cat 命令功能 格式&#xff1a;cat 文件名 2> 每次一个字符串的读写 fgets() 和 fputs() …