用最小的代价解决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 {
    @Bean
    public 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 {
			//限制批量保存最大的线程为5
			int 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) {
		//防止线程数太大,印制最大为20
		return 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;

	@Override
	public 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的批量保存

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

电子商务人工智能指南 4/6 - 内容理解

介绍 81% 的零售业高管表示&#xff0c; AI 至少在其组织中发挥了中等至完全的作用。然而&#xff0c;78% 的受访零售业高管表示&#xff0c;很难跟上不断发展的 AI 格局。 近年来&#xff0c;电子商务团队加快了适应新客户偏好和创造卓越数字购物体验的需求。采用 AI 不再是一…

Helm安装Mysql8主从复制集群

目录 一、Helm安装 二、安装mysql 1、拉取镜像 2、修改配置文件 3、创建mysql-secret 4、安装 一、Helm安装 这里不再赘叙&#xff0c;具体安装请参考官网 Helm | 快速入门指南 二、安装mysql 1、拉取镜像 #添加仓库 helm repo add bitnami https://charts.bitnami.c…

Java并发编程学习之从资本家的角度看多线程和并发性(一)

目录 前言前置知识一、单线程时代二、为什么要有多线程&#xff0c;多线程的优点&#xff1f;三、使用多线程会遇到什么问题&#xff1f;四、多线程和并发编程的关系总结 前言 这篇文章是打开Java多线程和并发编程的大门的开始&#xff0c;如标题《从老板的角度看多线程和并发…

【爬虫】selenium打开浏览器以及页面

本篇探讨如何使用 selenium 打开浏览器 selenium 基础与网页打开 selenium 是一个广泛应用于自动化测试和网页抓取的工具&#xff0c;它能够模拟用户在浏览器中的各种操作。首先&#xff0c;我们需要根据指定的浏览器类型&#xff08;这里以 Chrome 为例&#xff09;打开网页…

【算法练习】162. 寻找峰值

题目链接&#xff1a;162. 寻找峰值 看思路图&#xff1a; class Solution { public:int findPeakElement(vector<int>& nums) {int left 0,right nums.size()-1;while(left<right){int mid left (right-left)/2;if(nums[mid]>nums[mid1]){right mid;}els…

Android SurfaceFlinger layer层级

壁纸作为显示的最底层窗口它是怎么显示的 1. SurfaceFlinger layer层级 锁屏状态dump SurfaceFlinger &#xff0c;adb shell dumpsys SurfaceFlinger Display 0 (active) HWC layers: -----------------------------------------------------------------------------------…

SAP Ariba Approval _Email Approval

Email Approval Example 当用户成为文档审批者时,SAP Ariba会向该用户发送电子邮件通知消息。 在以下情况下,批准人可以收到电子邮件通知: 有人提交或重新提交文件以获得批准 某人撤回文件 系统升级文档 系统即将向主管升级请求 如果多个用户共享一个群组职责,他们则会收到…

vue 封装全局方法及使用

1.找到项目中的utils定义js&#xff0c;这个js存放全局可使用的方法 2.去项目中main.js中引入注册 import publicFun from ./utils/test Vue.prototype.$publicFun publicFun;3.项目使用 ddd(){this.$publicFun.testwen()},

MQTT消息服务器mosquitto介绍及说明

Mosquitto是一个开源的消息代理软件&#xff0c;支持MQTT协议&#xff08;消息队列遥测传输协议&#xff09;。MQTT是一种轻量级的发布/订阅消息传输协议&#xff0c;专为低带宽、不可靠网络环境下的物联网设备通信而设计。以下是关于Mosquitto服务器的一些介绍和说明&#xff…

(长期更新)《零基础入门 ArcGIS(ArcMap) 》实验一(下)----空间数据的编辑与处理(超超超详细!!!)

续上篇博客&#xff08;长期更新&#xff09;《零基础入门 ArcGIS(ArcMap) 》实验一&#xff08;上&#xff09;----空间数据的编辑与处理&#xff08;超超超详细&#xff01;&#xff01;&#xff01;&#xff09;-CSDN博客 继续更新 目录 什么是拓扑&#xff1f; 1.3.5道路拓…

深信服ATRUST与锐捷交换机端口链路聚合的配置

深信服ATRUST业务口原来只配置使用一个电口&#xff0c;近期出现流量达到800-900M接近端口的极限带宽。由于设备没有万光口&#xff0c;于是只好用2个光口来配置链接聚合。 下需附上深信服ATRST端口配置的截图&#xff0c;由于深信服ATRUST与锐捷交换机端口只共同支持源mac目的…

简易图书管理系统

javawebjspservlet 实体类 package com.ghx.entity;/*** author &#xff1a;guo* date &#xff1a;Created in 2024/12/6 10:13* description&#xff1a;* modified By&#xff1a;* version:*/ public class Book {private int id;private String name;private double pri…

【1】数据分析基础(一些概念)

数据分析的五步&#xff1a; &#xff08;1&#xff09;提出问题&#xff1b;&#xff08;2&#xff09;收集数据&#xff1b;&#xff08;3&#xff09;数据处理和清洗&#xff1b;&#xff08;4&#xff09;数据分析&#xff1b;&#xff08;5&#xff09;可视化&#xff0c…

Spring Boot 3.0 + MySQL 8.0 + kkFileView 实现完整文件服务

Spring Boot 3.0 MySQL 8.0 kkFileView 实现完整文件服务 背景&#xff1a;比较常见的需求&#xff0c;做成公共的服务&#xff0c;后期维护比较简单&#xff0c;可扩展多个存储介质&#xff0c;上传逻辑简单&#xff0c;上传后提供一个文件id&#xff0c;后期可直接通过此i…

文生图模型开源之光!ComfyUI - AuraFlow本地部署教程

一、模型介绍 AuraFlow 是唯一一个真正开源的文生图模型&#xff0c;由Fal团队开源&#xff0c;其代码和权重都放在了 FOSS 许可证下。基于 6.8B 参数优化模型架构&#xff0c;采用最大更新参数化技术&#xff0c;还重新标注数据集提升指令遵循质量。在物体空间和色彩上有优势…

OpenAI12天 –第3天的实时更新,包括 ChatGPT、Sora、o1 等

OpenAI提前开启了假期&#xff0c;推出了为期 12 天的活动&#xff0c;名为“OpenAI 12 天”。在接下来的一周左右的每一天&#xff0c;OpenAI 都将发布现有产品的新更新以及新软件&#xff0c;包括备受期待的 Sora AI 视频生成器。 OpenAI 首席执行官 Sam Altman 表示&#x…

06_掌握Python列表、元组、字典、集合

学习完本篇内容,你将掌握以下技能: 列表、元组、字典、集合的创建与删除列表、元组、字典、集合的访问及遍历列表、元组、字典、集合的操作方法列表、元组、字典、集合的生成式列表的基本操作 # 列表的基本操作 # 创建列表 list1 = [1, 2,

Ubuntu Server 22.04.5 LTS重启后IP被重置问题

Ubuntu Server 22.04.5 LTS重启后IP被重置问题 最近在使用Ubuntu Server 22.04做项目开发测试时发现每次重启和关机后&#xff0c;所设置的静态IP地址都会回复到安装系统时所设置的ip Ubuntu Server 22.04 官网下载地址&#xff1a;Ubuntu官方下载地址 对虚拟机下安装Ubuntu感…

QtCreator UI界面 菜单栏无法输入中文

如下图红色所示的区域&#xff0c;直接输入是无法输入中文的&#xff1a; 解决方法&#xff1a;在右边的属性值里输入即可 也可以参考这位同学的解决方法&#xff1a;友情链接

SCI论文丨机器学习与深度学习论文

目录 第一章、ChatGPT-4o使用方法与技巧 第二章、ChatGPT-4o辅助文献检索、总结与分析 第三章、ChatGPT-4o辅助学术论文选题、创新点挖掘与实验方案设计 第四章、ChatGPT-4o辅助学术论文开题与大纲生成 第五章、ChatGPT-4o辅助学术论文写作马拉松活动介绍 第六章、ChatGPT…