用最小的代价解决mybatis-plus关于批量保存的性能问题

1.问题说明

问题背景说明,在使用达梦数据库时,mybatis-plus的serviceImpl.saveBatch()方法或者updateBatchById()方法的时候,随着数据量、属性字段的增加,效率越发明显的慢。

serviceImpl.saveBatch();
serviceImpl.updateBatchById();

2.mysql的解决思路

如果你使用的是mysql的话,可以参考如下这个老哥的文章https://www.cnblogs.com/ajianbeyourself/p/18344695。改起来也简单,也就是配置参数加个属性。

spring.datasource.url=jdbc:mysql://localhost:3306/your_database?rewriteBatchedStatements=true

总结下就是:在 MyBatis-Plus 中启用 rewriteBatchedStatements 主要是为了提高批量插入/更新操作的性能。rewriteBatchedStatements 是 MySQL JDBC 驱动程序中的一个参数,用于将批量操作转换为单个 SQL 语句,以提高执行效率。

mysql的rewriteBatchedStatements属性,可以将多个SQL语句转化为一个sql语句。

这里我就不在啰嗦mysql的优化了,主要是针对其他数据库驱动没有rewriteBatchedStatements支持的情况下,我们该怎么优化,并且代价最小。

3.性能演示

以下代码是一个示例,先调用remove清空所有数据,然后记录开始时间,等待saveBatch以后,然后记录消耗时间

String oldData = JsonUtils.readFile("D:\\xxxx\\restdata\\" +"NR_RES_CHANNELROUTENODE_M" + ".json");
List<NrResChannelroutenodeM> list = JsonUtils.strToListBean(oldData, NrResChannelroutenodeM.class);
channelroutenodeMService.remove(null);
long start = System.currentTimeMillis();
channelroutenodeMService.saveBatch(list);
long end = System.currentTimeMillis() - start;
log.info("保存:{},数据总量:{},消耗时间:{}秒","List<NrResChannelroutenodeM>", list.size() , end / 1000f);

不然发现,使用mybatis-plus的原始saveBatch,基于NrResChannelroutenodeM这个实体来说,数据量约46万,消耗时间,大概110秒。这是在我本地的测试情况,实际上在用户现场的开发测试机上,这里的保存的时间已经超过了15分钟(原因有很多, 客户现场电脑配置较低,客户现场的部署环境有大概120个这样的批量保存,我这里只单独测试这一个所以只用110秒)。
在这里插入图片描述
接下来我们注释掉saveBatch,改成我自己编写的批量保存。这接近46万的数据量,我们切割成459份,一份保存1000条,1个线程保存需要17秒。

提升效果:110秒 -> 17秒

在这里插入图片描述
接下来,我改成500条数据一份,切割成918分,可以发现,性能还能更快,大概提升了2.5秒钟。也就是说,针对这个实体的数据来说,每次更新500条,比更新1000条快那么一小丢。

提升效果:17.1秒 -> 14.6秒
在这里插入图片描述

接下来,我将线程数提升到5:即5个线程运行,可以发现。时间来到了4.6秒,从最开始的110秒,到现在的4.6秒,这个提升很夸张了
提升效果:14.6秒 -> 4.6秒
在这里插入图片描述

4.优化思路

这里提一下,之前说的mysql是如何优化的。

mysql的rewriteBatchedStatements属性,可以将多个SQL语句转化为一个sql语句。

所以我思路也一样,如果是其他的非mysql,那就是把多个sql拼接成一个sql。

简单说,mybatis-plus执行批量保存到了数据库的时候,是下面这样的,

INSERT INTO users (name, email, age) VALUES ('John Doe', 'johndoe@example.com', 18);
INSERT INTO users (name, email, age) VALUES ('John Doe1', 'johndoe@example.com', 18);
INSERT INTO users (name, email, age) VALUES ('John Doe2', 'johndoe@example.com', 18);
INSERT INTO users (name, email, age) VALUES ('John Doe3', 'johndoe@example.com', 18);

而我们要的批量保存到了数据库执行的时候应该是下面这样的,

INSERT INTO users (name, email, age) VALUES ('John Doe', 'johndoe@example.com', 18),VALUES ('John Doe1', 'johndoe@example.com', 18),VALUES ('John Doe2', 'johndoe@example.com', 18),VALUES ('John Doe3', 'johndoe@example.com', 18);

伪代码示意

StringBuilder insert = new StringBuilder();
insert.append(INSERT INTO).append(表名);
insert.append(表字段);
for insert.append(字段对应的值);

4.1.准备一个数据库实体类

任意实体类即可,例如如下这种实体类。说明下实体类的要求,我们需要从这个实体类中提取出哪些信息来:
表名:获取@TableName(“SG_PULL_CONFIG”)或者获取类名
字段名:获取@TableId(“TABLE_NAME”)或者获取属性名

@Data
@TableName("SG_PULL_CONFIG")
public class SgPullConfig implements Serializable {private static final long serialVersionUID = 1L;@TableId("TABLE_NAME")private String tableName;@TableField("DATA_URL")private String dataUrl;@TableField(value = "CREATE_TIME", fill = FieldFill.INSERT)private Date createTime;
}

4.2.获取实体类对应的表名

	/*** @description:获取数据库实体类的的表名:TableName或者类名转驼峰* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2024年12月5日 上午11:10:01*/public static String getTableName(Object object) {String name ="";TableName annotation = object.getClass().getAnnotation(TableName.class);if(annotation != null) {name = annotation.value();}else {name = object.getClass().getSimpleName();name = QueryService.humpToLine(name);name = name.toUpperCase();}return name;}

4.3.获取数据表的属性字段

/*** @description:获取数据库实体类的字段名* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2024年12月5日 上午11:09:43*/public static List<String>  getFields(Object object){Field[] fields = object.getClass().getDeclaredFields();List<String> list = new ArrayList<>(fields.length);for (Field field : fields) {field.setAccessible(true);try {//TableId修饰的主键排除自增TableId tableId = field.getAnnotation(TableId.class);if (tableId != null && !IdType.AUTO.equals(tableId.type()))  {list.add(tableId.value());continue;}//TableField修饰的属性字段排除不存在的字段TableField tableField = field.getAnnotation(TableField.class);if (tableField != null && tableField.exist())  {list.add(tableField.value());continue;}//使用属性名和数据库字段名进行匹配的if(tableId == null && tableField == null && !"serialVersionUID".equals(field.getName())) {list.add(QueryService.humpToLine(field.getName()));}} catch (Exception e) {log.info("获取实体类的TableId和TableField异常");}}return list;}

4.4.获取数据表的属性值

这里需要注意一下:获取的属性值,需要对字符串和时间做特殊处理。如下图所示,看VALUES里面的部分,如果是字符串,我们需要添加单引号。
在这里插入图片描述

	/*** @description:获取数据库实体类的属性值* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2024年12月5日 上午11:09:17*/public static List<Object>  getdValues(Object object){Field[] fields = object.getClass().getDeclaredFields();List<Object> list = new ArrayList<>(fields.length);for (Field field : fields) {field.setAccessible(true);try {//TableId修饰的主键排除自增TableId tableId = field.getAnnotation(TableId.class);if ((tableId != null && !IdType.AUTO.equals(tableId.type())))  {list.add(getSqlValueByType(field.get(object), field));continue;}//TableField修饰的属性字段排除不存在的字段TableField tableField = field.getAnnotation(TableField.class);if (tableField != null && tableField.exist())  {list.add(getSqlValueByType(field.get(object), field));continue;}//使用属性名和数据库字段名进行匹配的if(tableId == null && tableField == null && !"serialVersionUID".equals(field.getName())) {list.add(getSqlValueByType(field.get(object), field));}} catch (Exception e) {log.info("获取实体类的TableId和TableField异常");}}return list;}private static Object getSqlValueByType(Object value, Field field) {if(value == null) {return null;}if(field.getType() == String.class) {return "'" + value + "'";}if(field.getType() == Date.class) {return "'" + DateUtils.getStrDate((Date)value, null) + "'";}return value;}

4.5.用:表名_字段名_字段值---->拼接SQL

	public static String getBatchInsertSql(List<?> list) {StringBuilder insert = new StringBuilder();String tableName = getTableName(list.get(0));List<String> fields = getFields(list.get(0));insert.append("INSERT INTO ").append(tableName).append("(");fields.forEach(temp -> insert.append(temp).append(","));insert.deleteCharAt(insert.length() - 1);insert.append(") VALUES ");StringBuilder valueTemp = null;for (Object temp : list) {valueTemp = new StringBuilder(); insert.append("(");List<Object> values = getdValues(temp);for (Object value : values) {valueTemp.append(value).append(",");}valueTemp.deleteCharAt(valueTemp.length() - 1);insert.append(valueTemp.toString());insert.append("),");}insert.deleteCharAt(insert.length() - 1);return insert.toString();}

4.6.执行拼接的sql语句

//注入SqlRunner 
@SpringBootConfiguration
@MapperScan(basePackages = "com.map.**.mapper")
@EnableTransactionManagement
public class MybatisConfig {@Beanpublic SqlRunner sqlRunner() {return new SqlRunner();}
}//SqlRunner 执行sql语句
String sql = getBatchInsertSql(list);
sqlRunner.insert(sql);

5.1多线程优化

目前为止我的代码如下,其中DB database无视就好了,这是我多数据源的时候切换数据源用的

  1. 先把数据切割成N份
  2. 创建线程池(最长线程数是)
  3. 现场池提交任务
  4. 主线程等待线程池的任务执行完毕
	/*** @description:批量保存* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2024年12月9日 下午2:36:10*/public <T> void saveBatch(IService<T> service, List<T> list, DB database) {if(ObjectUtils.isEmpty(list)) {log.error("list数据为空!");}if(list.size() <= DEFAULT_BATCH_SIZE) {service.saveBatch(list);}else {//限制批量保存最大的线程为5int batchThread = Math.min(list.size() / DEFAULT_BATCH_SIZE, 5);bigDataSave(list, database , DEFAULT_BATCH_SIZE, batchThread);}}/*** @description:大数据量的数据保存* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2024年12月4日 下午4:43:37*/public void bigDataSave(List<?> list, DB database, int size ,int thread) {if(ObjectUtils.isEmpty(list)) {return;}List<List<?>> splitList = ArraysUtils.splitList(list, size);log.info("当前数据量:{},切割:{}份", list.size(), splitList.size());ExecutorService executor = threadPoolService.newFixedThreadPool(thread);for (int i = 0; i < splitList.size(); i++) {executor.submit(new BigDataTask(splitList.get(i), database, sqlRunner));}threadPoolService.shutdownAndWait(executor);}

线程池配置

@Component
public class ThreadPoolService {//最大线程数private int maximumPoolSize = 20;public ThreadPoolService threadPoolService() {return new ThreadPoolService();}public ExecutorService newFixedThreadPool(int nThreads) {//防止线程数太大,印制最大为20return new ThreadPoolExecutor(nThreads, Math.min(nThreads, maximumPoolSize),0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}public ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, maximumPoolSize,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}public void shutdownAndWait(ExecutorService executor) {executor.shutdown();while (!executor.isTerminated()){};}}

多线程的子任务

@Log4j2
@AllArgsConstructor
public class BigDataTask implements Runnable {//以下属性使用构造方法注入进来的,因为自己new BigDataTask,BigDataTask不是spring托管,因此无法使用Spring注入进来private List<?> list;private DB database;private SqlRunner sqlRunner;@Overridepublic void run() {//这个代码是用来切换数据源的DataSourceContextHolder.setDataSource(database.getEnumId());try {String sql = SaveBatchSerive.getBatchInsertSql(list);sqlRunner.insert(sql);} catch (Exception e) {log.info("任务:BigDataTask,处理失败,失败原因:{}", e);}DataSourceContextHolder.removeDataSource();}
}

在批量保存的地方调用即可,如下所示
在这里插入图片描述
优点:

  1. 入门难度低,不需要对mybatis-plus做任何修改,减少对mybatis-plus技术的研究工作量
  2. 操作可控,仅针对性能有问题的地方将xxxxService.saveBatch(list)改为我们自己编写的saveBatchSerive.saveBatch()即可,是局部性,而不是全局的,不至于出现为了修改某个地方的saveBatch()导致所有的saveBatch()都出现问题
  3. 可以合理自己配置适合自己的线程数以提升效率(并不是线程数越多越好)详情可以看我以前的介绍:系统适合开启多少线程数量?
saveBatchSerive.saveBatch(channelroutenodeMService, list, DB.从库);
//saveBatchSerive.bigDataSave(list, DB.从库 , 500, 1);
//channelroutenodeMService.saveBatch(list);

缺点:
1.没有在底层修改,如果开发团队其他开发成员调用原生的mybatis-plus,saveBatch时,还会出现性能问题
2.无法对已经编写的代码进行优化,需要将历史代码中的saveBatch替换成自己的。

5其他优化方式-替换saveBatch

具体实现方式参考:我这里就不废话了,但是我并不推荐这种方式,
https://openatomworkshop.csdn.net/6645aa50b12a9d168eb6bd90.html
大概思路如下:

  1. 编写一个RootMapper/RootService来替换原来的BaseMapper/IService
  2. 自己编写批量保存代码
  3. 业务Mapper/业务Service继承(实现)时,用RootMapper、RootService
  4. 批量保存的时候,用的是RootMapper的批量保存,不是BaseMapper的批量保存

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

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

相关文章

OpenCV相机标定与3D重建(10)眼标定函数calibrateHandEye()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算手眼标定&#xff1a; g T c _{}^{g}\textrm{T}_c g​Tc​ cv::calibrateHandEye 是 OpenCV 中用于手眼标定的函数。该函数通过已知的机器人…

flink yarn模式3种提交任务方式

接上文&#xff1a;一文说清flink从编码到部署上线 1.引言 Apache Hadoop的Yarn是许多数据处理框架中非常流行的资源提供者。Flink的服务提交给Yarn的ResourceManager后&#xff0c;ResourceManager会在由Yarn的NodeManager管理的机器上动态分配运行容器。Flink在这些容器上部…

32.最长有效括号 python

最长有效括号 题目题目描述示例 1&#xff1a;示例 2&#xff1a;示例 3&#xff1a;提示&#xff1a;题目链接 题解算法步骤&#xff1a;python实现解释&#xff1a;提交结果 题目 题目描述 给你一个只包含 ‘(’ 和 ‘)’ 的字符串&#xff0c;找出最长有效&#xff08;格式…

OpenCV相机标定与3D重建(13)检测给定图像中是否存在符合指定尺寸的棋盘格图案函数checkChessboard()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::checkChessboard 是 OpenCV 库中的一个函数&#xff0c;用于检测给定图像中是否存在符合指定尺寸的棋盘格图案。这个函数对于相机校准非常重…

规范秩相关信息搜集Day2

系列博客目录 文章目录 系列博客目录1.A Survey on Tensor Techniques and Applications in Machine Learning2.有没有研究低秩矩阵有利于分类的计算机方面的论文呢3.Image classification based on low-rank matrix recovery and Naive Bayes collaborative representatio 基于…

2024年华中杯数学建模C题基于光纤传感器的平面曲线重建算法建模解题全过程文档及程序

2024年华中杯数学建模 C题 基于光纤传感器的平面曲线重建算法建模 原题再现 光纤传感技术是伴随着光纤及光通信技术发展起来的一种新型传感器技术。它是以光波为传感信号、光纤为传输载体来感知外界环境中的信号&#xff0c;其基本原理是当外界环境参数发生变化时&#xff0c…

【Golang】Go语言编程思想(六):Channel,第二节,使用Channel等待Goroutine结束

使用 Channel 等待任务结束 首先回顾上一节 channel 这一概念介绍时所写的代码&#xff1a; package mainimport ("fmt""time" )func worker(id int, c chan int) {for n : range c {fmt.Printf("Worker %d received %c\n",id, n)} }func crea…

【Windows】【P2P】ipv6 nmap ncat 测试电信、移动、联通两个4G 5G热点ipv6地址的连通性

测试场景 一台PC在电信4G热点下&#xff0c;一台PC在电信5G热点下。 扩展测试 电信、移动、联通的ipv6 下载安装nmap Download the Free Nmap Security Scanner for Linux/Mac/Windows 安装后&#xff0c;进入目录C:\Windows\System32\WindowsPowerShell\v1.0\powershell.e…

一文掌握 OpenGL 几何着色器的使用

学习本文需要具备 OpenGL ES 编程基础,如果看起来比较费劲,可以先看入门文章 OpenGL ES 3.0 从入门到精通系统性学习教程 。 什么是几何着色器 几何着色器(Geometry Shader) OpenGL 管线中的可选着色器阶段,位于顶点着色器(Vertex Shader) 和光栅化阶段 之间。 其核心…

C—初阶调试

对你有帮助的话能否一键三连啊&#xff01;祝每个人心想事成&#xff01; 什么是Bug? 首先我们先了解一下日常口语中的“Bug”是什么 Bug可以理解为计算机程序错误&#xff0c;编程时的漏洞 调试及重要性 顾名思义&#xff0c;调试就是通过工具找出bug存在&#xff0c;找出…

Capacitor 打包后的 iOS app 无法访问 http 的内容,解决办法

Capacitor 打包后的 iOS app 无法访问 http 的内容&#xff0c;解决办法 上篇文章中说了如何使用 Capacitor 打包成 iOS app 的过程中遇到的问题 Capacitor在 xcode 打包 iOS 应用发布的时候出错。 在这之后&#xff0c;遇到了一个新问题&#xff0c; 就是它无法访问 http 的内…

LLaMA Factory+ModelScope实战——使用 Web UI 进行监督微调

LLaMA FactoryModelScope实战——使用 Web UI 进行监督微调 文章原始地址&#xff1a;https://onlyar.site/2024/01/14/NLP-LLaMA-Factory-web-tuning/ 引言 大语言模型微调一直都是一个棘手的问题&#xff0c;不仅因为需要大量的计算资源&#xff0c;而且微调的方法也很多。在…

Excel的文件导入遇到大文件时

Excel的文件导入向导如何把已导入数据排除 入起始行&#xff0c;选择从哪一行开始导入。 比如&#xff0c;前两行已经导入了&#xff0c;第二次导入的时候排除前两行&#xff0c;从第三行开始&#xff0c;就将导入起始行设置为3即可&#xff0c;且不勾选含标题行。 但遇到大文…

【C++】选择排 序算法分析与扩展

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;代码回顾&#x1f4af;选择排序的算法流程&#x1f4af;代码详解外层循环初始化最小值内层循环比较与更新元素交换 &#x1f4af;选择排序的特性时间复杂度空间复杂度稳定性…

顺序表(数据结构初阶)

文章目录 顺序表一&#xff1a;线性表1.1概念&#xff1a; 二&#xff1a;顺序表2.1概念与结构&#xff1a;2.2分类&#xff1a;2.2.1静态顺序表2.2.2动态顺序表 2.3动态顺序表的实现声明&#xff08;初始化&#xff09;检查空间容量尾插头插尾删头删查找指定位置之前插入数据指…

【Linux】磁盘结构和文件系统

文章目录 磁盘磁盘的物理结构LBA寻址法抽象管理分区化总结 磁盘 磁盘是计算机存储系统的核心部件之一&#xff0c;主要用于长期存储数据。磁盘的基本概念、物理结构和逻辑组织形式直接影响着其性能和使用效率。 下面的图片是一个磁盘&#xff1a; 磁盘打开之后的结构如下&…

NLP-中文分词

中文分词 1、中文分词研究背景及意义 和大部分西方语言不同&#xff0c;书面汉语的词语之间没有明显的空格标记&#xff0c;句子是以字串的形式出现。因此对中文进行处理的第一步就是进行自动分词&#xff0c;即将字串转变成词串。 比如“中国建筑业呈现新格局”分词后的词串…

【Golang】Go语言编程思想(六):Channel,第三节,使用Channel实现树的遍历

使用 Channel 实现树的遍历 tree 在此处简单回顾一下之前学过的二叉树遍历&#xff0c;首先新建一个名为 tree 的目录&#xff0c;并在其下对文件和子目录进行如下组织&#xff1a; 其中 node.go 存放的是 Node 的定义&#xff1a; package treeimport "fmt"type…

spring 源码分析

1 IOC 源码解析 BeanDefinition: bean的定义。里面会有beanClass、beanName、scope等属性 beanClass&#xff1a;通过Class.forName生成的Class 对象beanName&#xff1a;context.getBean(“account”)&#xff0c;acount就是beanNamescope: 作用区分单例bean、原型bean Bea…

快速搭建SpringBoot3+Vue3+ElementPlus管理系统

快速搭建SpringBoot3Vue3管理系统 前端项目搭建&#xff08;默认开发环境&#xff1a;node20,Jdk17&#xff09;创建项目并下载依赖--执行以下命令 前端项目搭建&#xff08;默认开发环境&#xff1a;node20,Jdk17&#xff09; 创建项目并下载依赖–执行以下命令 创建项目 y…