SpringBoot百万行Excel导入MySQL实践

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

一、前期准备

  1. 首先我们选用的肯定是阿里出品的EasyExcel,对比poi和jxl占内存更少
    easyexcel官方网站
  2. 准备测试的数据库和excel文件,已经和代码一起上传到gitee仓库
    项目代码
  3. 修改mysql的max_allowed_packet
    解决MySQL的PacketTooBigException异常问题
  4. 修改了tomcat上传文件的默认限制,因为文件可能过大,会报错
server:port: 8888maxHttpHeaderSize: 102400servlet:context-path: /apierror:include-exception: falseinclude-message: always
spring:servlet:multipart:max-file-size: 100MBmax-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 {@ResourceUserService 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() {}@Overridepublic void invoke(User user, AnalysisContext analysisContext) {if (user != null) {list.add(user);}}@Overridepublic 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() {}@Overridepublic 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();}@Overridepublic 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() {}@Overridepublic void invoke(User user, AnalysisContext analysisContext) {if (user != null) {list.add(user);}}@Overridepublic 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;}@Overridepublic 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.rhkb.cn/news/408275.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

-Wl,-rpath= 编译器链接器指定动态库路径 与 LD_LIBRARY_PATH

实例先行&#xff0c; 1&#xff0c;情景 三互相依赖的小项目&#xff1a; &#xff08;1&#xff09;libbottom.so&#xff0c;无特别依赖&#xff0c;除系统文件 &#xff08;2&#xff09;libtop.so&#xff0c;依赖libbottom.so &#xff08;3&#xff09;app 可执行程…

springboot admin监控

服务端搭建 maven的依赖&#xff0c;包括服务端和客户端&#xff0c;以及注册到nacos上面 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XML…

AI绘制思维导图:使用SpringBoot和Vue实现智能可视化

目录 引言&#xff1a; 思维导图的重要性和应用场景&#xff1a; AI在思维导图绘制中的应用&#xff1a; 概述SpringBoot和Vue框架的特点&#xff1a; 第一部分&#xff1a;思维导图概述 思维导图的定义和历史 思维导图的结构和组成部分 思维导图在不同领域的应用案例 …

Linux 进程 | 进程地址空间

文章目录 进程地址空间程序地址空间进程地址空间 进程地址空间 程序地址空间 地址空间一共有如下的几个区域&#xff0c;从下到上地址逐渐增加&#xff0c;其中栈区的空间是从上往下使用&#xff0c;即从高地址往低地址增长&#xff1b;堆区的空间是从下往上使用&#xff0c;…

【鸿蒙学习】HarmonyOS应用开发者高级认证 - 应用DFX能力介绍(含闯关习题)

学完时间&#xff1a;2024年8月24日 学完排名&#xff1a;第1698名 一、Performance Analysis Kit简介 Performance Analysis Kit&#xff08;性能分析服务&#xff09;为开发者提供应用事件、日志、跟踪分析工具&#xff0c;可观测应用运行时状态&#xff0c;用于行为分析、…

Prometheus学习

监控架构介绍&#xff1a; 基本架构&#xff1a; Prometheus 和 Zabbix 的对比&#xff1a; 安装和使用&#xff1a; Prometheus 采集、存储数据Grafana 用于图表展示alertmanager 用于接收 Prometheus 发送的警告信息node-exporter 用于收集操作系统和硬件信息的 metrics …

Linux:Bash中的命令介绍(简单命令、管道以及命令列表)

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 在Bash中&#xff0c;命令执行的方式可以分为简单命令、管道和命令列表组成。这些结构提供了强大的工具&#xff0c;允许用户组合命令并精确控制其执行方式。以下是对这…

Ubuntu24.04安装MYSQL8.0

更新源 sudo apt update安装mysql服务 默认安装最新版本 sudo apt install mysql-server检查安装版本 mysql --version检查mysql运行状态 systemctl status mysql开启远程访问&#xff0c;在ubuntu下mysql默认是只允许本地访问 sudo vim /etc/mysql/mysql.conf.d/mysqld.…

新疆旅游今年为什么这么火热?

今年新疆旅游火爆全网&#xff0c;不夸张的说&#xff0c;打开朋友圈&#xff0c;几乎一半人在新疆旅游、还有一半人在去新疆旅游的路上。 大家也纷纷在小红书上晒出新疆相关的笔记&#xff0c;覆盖旅游、美食、穿搭、养生、摄影等众多热门行业&#xff0c;相关话题多次登上小…

【C++】12.智能指针

在上一篇博客【C】11.异常中我们知道有些时候会造成内存空间的未释放从而导致内存泄漏&#xff0c;因此本篇博客的内容就是如何减少内存泄漏——智能指针。 一、RAII RAII&#xff08;Resource Acquisition Is Initialization&#xff09;是一种利用对象生命周期来控制程序资…

垃圾分类笔记YOLOV5(一)-pip换源-口罩识别-训练自己的数据集

pip换源网址 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple不进行配置的是临时换源 1、从github上下载YOLOV5的代码 翻墙软件clash 数据集地址roboflow clash配置一键导入 哔哩哔哩视频地址 数据集的下载格式&#xff1a; 2、修改自己的数据…

Webots与ROS1、ROS2接口变迁-2024-

三大免费仿真器CoppeliaSim、Gazebo和Webots。 Gazebo接口总结&#xff1a; Gazebo与ROS1、ROS2接口变迁-2005-2024--CSDN博客 缺点&#xff1a;版本绑定策略 早期webots版本和ros版本绑定 后期&#xff0c;webots接口最新版本和ros特定版本最匹配。 例如&#xff1a; 最好按…

Scrapy 分布式爬虫框架 Scrapy-Redis

github官网代码示例&#xff1a;https://github.com/rmax/scrapy-redis/blob/master/example-project/example/spiders/myspider_redis.py 什么是 Scrapy-Redis Scrapy-Redis 是一个基于 Scrapy 的扩展&#xff0c;用于实现分布式爬虫。它利用 Redis 作为分布式队列来共享待爬…

R 语言学习教程,从入门到精通,R 绘图饼图(23)

1、R 绘图 条形图 条形图&#xff0c;也称为柱状图条形图&#xff0c;是一种以长方形的长度为变量的统计图表。 条形图可以是水平或垂直的&#xff0c;每个长方形可以有不同的颜色。 R 语言使用 barplot() 函数来创建条形图&#xff0c;格式如下&#xff1a; barplot(H,xlab,…

JavaScript初级——DOM和事件简介

一、什么是DOM&#xff1f; 二、模型 三、对象的 HTML DOM 树 四、节点 浏览器已经为我们提供了文档节点对象&#xff0c;这个对象是window属性&#xff0c;可以再网页中直接使用&#xff0c;文档节点代表的是整个网页。 五、事件简介 事件&#xff0c;就是用户和浏览器之间的交…

【每日一题】【素数筛板子题】又是一年毕业季 牛客小白月赛99 D题 C++

牛客小白月赛99 D题 又是一年毕业季 题目背景 牛客小白月赛99 题目描述 样例 #1 样例输入 #1 3 4 2 4 6 5 5 6 2 5 3 2333333 8 11 4 5 14 19 19 8 10样例输出 #1 3 7 2做题思路 首先观察到 即需要保证拍照的时刻 大于等于 2 那么就从2开始往上走&#xff0c;如果有人…

【精选】推荐7款AI论文一键生成论文、开题报告和文献综述网站

在当前的学术研究和写作中&#xff0c;AI技术的应用已经变得越来越普遍。特别是对于论文、开题报告和文献综述的生成&#xff0c;许多平台提供了便捷的一键生成服务。以下是七款推荐的AI论文一键生成工具&#xff0c;包括千笔-aipaperpass。 1. 千笔-aipaperpass 千笔-aipape…

文心快码(Baidu Comate)初体验

文心快码&#xff08;Baidu Comate&#xff09;初体验 1文心快码简介和安装&#xff1a;简要介绍文心快码&#xff08;Baidu Comate&#xff09;、安装方法、使用方法等&#xff1b; Baidu Comate 是由百度自主研发&#xff0c;基于文心大模型&#xff0c;结合百度丰富的编程现…

主机安全-网络攻击监测

目录 概述暴力破解&#xff08;SSH爆破为例&#xff09;原理规则攻击模拟告警 端口扫描原理规则攻击模拟告警 流量劫持原理规则攻击模拟告警 参考 概述 本文介绍主机网络层面上的攻击场景&#xff0c;每种攻击场景举一个例子。监测方面以字节跳动的开源HIDS elkeid举例。 针对…

当前A股平均市盈率

再写一篇【不务正业】的 2023-08-23A股平均市盈率 来自乐咕乐股网 当前A股市盈率是否为低点&#xff1f; 不言而喻 ‌当前A股市场的市盈率确实处于相对低位。‌ 根据东方财富Choice最新数据显示数据&#xff0c;截至2024年8月23日&#xff0c;全A市盈率为13.06倍&#xff0c;…