mybatis-plus 的saveBatch性能分析

Mybatis-Plus 的批量保存saveBatch 性能分析

目录

  • `Mybatis-Plus` 的批量保存`saveBatch` 性能分析
    • 背景
    • 批量保存的使用方案
      • 循环插入
      • 使用`PreparedStatement `预编译
        • 优点:
        • 缺点:
    • `Mybatis-Plus `的`saveBatch`
    • `Mybatis-Plus`实现真正的批量插入
      • 自定义`sql`注入器
      • 定义通用`mapper``CommonMapper`
      • 将自定义的注入器加载到容器中
      • 业务`mapper`
      • 测试
      • 优化
    • 执行性能比较
      • `rewriteBatchedStatements` 参数分析

背景

昨天同事问我,mybatis-plus 自动生成的service 里面提供的savebatch 最后生成的批量插入语句是多条insert ,而不是insert...vaues (),()的语句,这样是不是跟我们使用循环调用没区别,这样的批量插入是不是有性能问题?下面我们就此问题来进行分析一下。

批量保存的使用方案

循环插入

使用 for 循环一条一条的插入,这个方式比较简单直观,灵活,但是这个 对于大型数据集,使用for循环逐条插入数据可能会导致性能问题,特别是在网络延迟高或数据库负载大的情况下。使用for循环进行数据插入时,需要注意事务管理,确保数据的一致性和完整性。如果不适当地管理事务,可能会导致数据不一致或丢失。而且每次循环迭代都需要建立和关闭数据库连接,这可能会导致额外的数据库连接开销,影响性能。

使用PreparedStatement 预编译

使用预处理的方式进行批量插入是一种常见的优化方法,它可以显著提高插入操作的性能。

优点:
  • 性能提升: 预处理可以减少每次插入操作中的数据库通信次数,从而降低了网络通信的开销,提高了插入操作的效率和性能。

  • 减少数据库负载: 将多条数据组合成批量插入的方式可以减少数据库服务器的负载,降低了数据库系统的压力,有助于提高整个系统的性能。

  • 减少连接开销: 预处理可以减少每次循环迭代中建立和关闭数据库连接的开销,从而节省了系统资源,提高了连接的复用率。

  • 事务管理:可以将多个插入操作放在一个事务中,以确保数据的一致性和完整性,并在发生错误时进行回滚,从而保证数据的安全性。

缺点:
  • 内存消耗: 将多条数据组合成批量插入的方式可能会增加内存消耗,特别是在处理大量数据时。因此,需要注意内存的使用情况,以避免内存溢出或性能下降。

  • 数据格式转换: 在将数据组合成批量插入时,可能需要进行数据格式转换或数据清洗操作,这可能会增加代码的复杂度和维护成本。

  • 可读性降低: 预处理方式可能会使代码结构变得复杂,降低了代码的可读性和可维护性,特别是对于一些初学者或新加入团队的开发人员来说可能会造成困扰

所以由此可见预编译方式性能较好,如果想避免内存问题的话,其实使用分批插入也可以解决这个问题。

Mybatis-Plus saveBatch

直接看源码

    /*** 批量插入** @param entityList ignore* @param batchSize  ignore* @return ignore*/@Transactional(rollbackFor = Exception.class)@Overridepublic boolean saveBatch(Collection<T> entityList, int batchSize) {String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));}/*** 执行批量操作** @param entityClass 实体类* @param log         日志对象* @param list        数据集合* @param batchSize   批次大小* @param consumer    consumer* @param <E>         T* @return 操作结果* @since 3.4.0*/public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {Assert.isFalse(batchSize < 1, "batchSize must not be less than one");return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {int size = list.size();int idxLimit = Math.min(batchSize, size);int i = 1;for (E element : list) {consumer.accept(sqlSession, element);if (i == idxLimit) {sqlSession.flushStatements();idxLimit = Math.min(idxLimit + batchSize, size);}i++;}});}

通过代码可以发现2个点,第一个就是批量保存的时候会默认进行分批,每批的大小为1000条数据;第二点就是通过代码

return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));

 for (E element : list) {consumer.accept(sqlSession, element);if (i == idxLimit) {sqlSession.flushStatements();idxLimit = Math.min(idxLimit + batchSize, size);}i++;}

可以看出插入是循环插入,并没有进行拼接处理。但是这里唯一不同与循环插入的是可以看到这里是通过sqlSession.flushStatements()将一个个单条插入的insert语句分批次进行提交,用的是同一个sqlSession

这里其实就可以看出来mybatis-plus的批量插入实际上不是真正意义上的批量插入。那如果想实现真正的批量插入就只能手动拼接脚本吗?其实mybatis-plus提供了sql注入器,我们可以自定义方法来满足业务的实际开发需求。官方文档:https://baomidou.com/pages/42ea4a/

在这里插入图片描述

Mybatis-Plus实现真正的批量插入

自定义sql注入器

/*** @author leo* @date 2024年03月13日 15:16*/
public class BatchSqlInjector extends DefaultSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {List<AbstractMethod> methodList = super.getMethodList(mapperClass,tableInfo);//更新时自动填充的字段,不用插入值methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));return methodList;}
}

定义通用mapper``CommonMapper

/*** @author leo* @date 2024年03月13日 16:34*/
public interface CommonMapper<T> extends BaseMapper<T> {/*** 真正的批量插入* @param entityList* @return*/int insertBatch(List<T> entityList);
}

将自定义的注入器加载到容器中

/*** @author leo* @date 2024年03月13日 15:41*/
@Configuration
public class MybatisPlusConfig {@Beanpublic BatchSqlInjector sqlInjector() {return new BatchSqlInjector();}
}

业务mapper

/**** @author leo* @since 2024-01-11*/
public interface LlfInfoMapper extends CommonMapper<LlfInfoEntity> {}

测试

List<LlfInfoEntity> llfInfoEntities = new ArrayList<>();for (int i = 0; i <= 10; i++) {LlfInfoEntity llfInfoEntity = new LlfInfoEntity();llfInfoEntity.setChannelNum(i + "");llfInfoEntity.setGroupNumber(i+"");llfInfoEntity.setFlight(i+1);llfInfoEntity.setIdNumber(i+"sadsadsad");llfInfoEntities.add(llfInfoEntity);}llfInfoMapper.insertBatch(llfInfoEntities);

这里我们看下控制台打印的语句:

在这里插入图片描述
很明显,达到了我们的效果。

优化

这里可以看到InsertBatchSomeColumn 方法没有批次的概念,如果没有批次的话,那这里地方可能会有性能问题,你想想如果这个条数无穷大的话,我那这个sql语句会非常大,不仅会超出mysql的执行sql的长度限制,也会造成oom。那么这里我们就需要自己实现一下批次插入了,不知道大家还有没有印象前面的saveBatch()方法是怎么实现批次插入的。我们也可以参考一下实现方式。直接上代码

    public  boolean executeBatch(Collection<LlfInfoEntity> list, int batchSize) {int size = list.size();int idxLimit = Math.min(batchSize, size);int i = 1;List<LlfInfoEntity> batchList = new ArrayList<>();for (LlfInfoEntity element : list) {batchList.add(element);if (i == idxLimit) {llfInfoMapper.insertBatchSomeColumn(batchList);batchList.clear();idxLimit = Math.min(idxLimit + batchSize, size);}i++;}return true;}

测试代码:

        List<LlfInfoEntity> llfInfoEntities = new ArrayList<>();for (int i = 0; i <= 10; i++) {LlfInfoEntity llfInfoEntity = new LlfInfoEntity();llfInfoEntity.setChannelNum(i + "");llfInfoEntity.setGroupNumber(i + "");llfInfoEntity.setFlight(i + 1);llfInfoEntity.setIdNumber(i + "sadsadsad");llfInfoEntities.add(llfInfoEntity);}executeBatch(llfInfoEntities,5);

看执行结果:

在这里插入图片描述

这里就实现了真正的批量插入了。

执行性能比较

这里我就不去具体展现测试数据了,直接下结论了。

首先最快的肯定是手动拼sql脚本和mybatis-plus的方式速度最快,其次是mybatis-plussaveBatch。这里要说下有很多文章都说需要单独配置rewriteBatchedStatements参数,才会启用saveBatch的批量插入方式。但是我这边跟进源码进行查看的时候默认值就是true,所以我猜测可能是版本问题,下面会附上版本以及源码供大家参考。

rewriteBatchedStatements 参数分析

首选我们通过com.baomidou.mybatisplus.extension.toolkit.SqlHelper#executeBatch(java.lang.Class<?>, org.apache.ibatis.logging.Log, java.util.Collection<E>, int, java.util.function.BiConsumer<org.apache.ibatis.session.SqlSession,E>)l里面的sqlSession.flushStatements();代码可以跟踪到,mysql驱动包里面的com.mysql.cj.jdbc.StatementImpl#executeBatch下面这段代码

 @Overridepublic int[] executeBatch() throws SQLException {return Util.truncateAndConvertToInt(executeBatchInternal());}protected long[] executeBatchInternal() throws SQLException {JdbcConnection locallyScopedConn = checkClosed();synchronized (locallyScopedConn.getConnectionMutex()) {if (locallyScopedConn.isReadOnly()) {throw SQLError.createSQLException(Messages.getString("Statement.34") + Messages.getString("Statement.35"),MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());}implicitlyCloseAllOpenResults();List<Object> batchedArgs = this.query.getBatchedArgs();if (batchedArgs == null || batchedArgs.size() == 0) {return new long[0];}// we timeout the entire batch, not individual statementsint individualStatementTimeout = getTimeoutInMillis();setTimeoutInMillis(0);CancelQueryTask timeoutTask = null;try {resetCancelledState();statementBegins();try {this.retrieveGeneratedKeys = true; // The JDBC spec doesn't forbid this, but doesn't provide for it either...we do..long[] updateCounts = null;if (batchedArgs != null) {int nbrCommands = batchedArgs.size();this.batchedGeneratedKeys = new ArrayList<>(batchedArgs.size());boolean multiQueriesEnabled = locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.allowMultiQueries).getValue();if (multiQueriesEnabled || this.rewriteBatchedStatements.getValue() && nbrCommands > 4) {return executeBatchUsingMultiQueries(multiQueriesEnabled, nbrCommands, individualStatementTimeout);}timeoutTask = startQueryTimer(this, individualStatementTimeout);updateCounts = new long[nbrCommands];for (int i = 0; i < nbrCommands; i++) {updateCounts[i] = -3;}SQLException sqlEx = null;int commandIndex = 0;for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {try {String sql = (String) batchedArgs.get(commandIndex);updateCounts[commandIndex] = executeUpdateInternal(sql, true, true);if (timeoutTask != null) {// we need to check the cancel state on each iteration to generate timeout exception if neededcheckCancelTimeout();}// limit one generated key per OnDuplicateKey statementgetBatchedGeneratedKeys(this.results.getFirstCharOfQuery() == 'I' && containsOnDuplicateKeyInString(sql) ? 1 : 0);} catch (SQLException ex) {updateCounts[commandIndex] = EXECUTE_FAILED;if (this.continueBatchOnError && !(ex instanceof MySQLTimeoutException) && !(ex instanceof MySQLStatementCancelledException)&& !hasDeadlockOrTimeoutRolledBackTx(ex)) {sqlEx = ex;} else {long[] newUpdateCounts = new long[commandIndex];if (hasDeadlockOrTimeoutRolledBackTx(ex)) {for (int i = 0; i < newUpdateCounts.length; i++) {newUpdateCounts[i] = java.sql.Statement.EXECUTE_FAILED;}} else {System.arraycopy(updateCounts, 0, newUpdateCounts, 0, commandIndex);}sqlEx = ex;break;//throw SQLError.createBatchUpdateException(ex, newUpdateCounts, getExceptionInterceptor());}}}if (sqlEx != null) {throw SQLError.createBatchUpdateException(sqlEx, updateCounts, getExceptionInterceptor());}}if (timeoutTask != null) {stopQueryTimer(timeoutTask, true, true);timeoutTask = null;}return (updateCounts != null) ? updateCounts : new long[0];} finally {this.query.getStatementExecuting().set(false);}} finally {stopQueryTimer(timeoutTask, false, false);resetCancelledState();setTimeoutInMillis(individualStatementTimeout);clearBatch();}}}

我们主要核心看一下这个代码:

  if (multiQueriesEnabled || this.rewriteBatchedStatements.getValue() && nbrCommands > 4) {return executeBatchUsingMultiQueries(multiQueriesEnabled, nbrCommands, individualStatementTimeout);}

能进入if语句,并执行批处理方法 executeBatchUsingMultiQueryies 的条件如下:

  • allowMultiQueries = true
  • rewriteBatchedStatements=true
  • 数据总条数 > 4条

PropertyKey.java中定义了 multiQueriesEnablesrewriteBatchedStatements 的枚举值,com.mysql.cj.conf.PropertyKey如下:

在这里插入图片描述
在这里插入图片描述

可以看出这个参数都是true。所以我这边默认就是支持批量操作的。

mybatis-plus 版本:3.5.10

mysql-connector-java版本:8.0.31

Queryies` 的条件如下:

  • allowMultiQueries = true
  • rewriteBatchedStatements=true
  • 数据总条数 > 4条

PropertyKey.java中定义了 multiQueriesEnablesrewriteBatchedStatements 的枚举值,com.mysql.cj.conf.PropertyKey如下:

[外链图片转存中…(img-nwh8oV0y-1710751858305)]

[外链图片转存中…(img-AmPKylvo-1710751858305)]

可以看出这个参数都是true。所以我这边默认就是支持批量操作的。

mybatis-plus 版本:3.5.10

mysql-connector-java版本:8.0.31

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

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

相关文章

Linux进程通信补充——System V通信

三、System V进程通信 ​ System V是一个单独设计的内核模块&#xff1b; ​ 这套标准的设计不符合Linux下一切皆文件的思想&#xff0c;尽管隶属于文件部分&#xff0c;但是已经是一个独立的模块&#xff0c;并且shmid与文件描述符之间的兼容性做的并不好&#xff0c;网络通…

华为汽车业务迎关键节点,长安深蓝加入HI模式,车BU预计今年扭亏

‍编辑 |HiEV 一年之前&#xff0c;同样是在电动汽车百人会的论坛上&#xff0c;余承东在外界对于华为和AITO的质疑声中&#xff0c;第一次公开阐释了华为选择走智选车模式的逻辑。 一年之后&#xff0c;伴随问界M7改款、问界M9上市&#xff0c;华为智选车模式的面貌已经发生了…

使用jenkins-pipeline进行利用项目文件自动化部署到k8s上

Discard old builds:丢弃旧的构建,目的是管理存储空间、提升性能以及保持环境整洁 Do not allow concurrent builds: 禁止并发构建是指同一时间内只允许一个构建任务执行,避免多个构建同时运行可能带来的问题 Do not allow the pipeline to resume if the controller resta…

信息系统项目管理师018:计算机网络(2信息技术发展—2.1信息技术及其发展—2.1.2计算机网络)

文章目录 2.1.2 计算机网络1.网络标准协议2.软件定义网络3.第五代移动通信技术 记忆要点总结 2.1.2 计算机网络 在计算机领域中&#xff0c;网络就是用物理链路将各个孤立的工作站或主机相连在一起&#xff0c;组成数据链路&#xff0c;从而达到资源共享和通信的目的。凡将地理…

SpringCloud搭建微服务之Micrometer分布式链路追踪

1. 概述 由于Spring Cloud Sleuth最新版本只支持Spring Boot 2.7.x&#xff0c;核心项目已经迁移到Micrometer Traceing项目&#xff0c;Spring Boot 3.x版本要实现分布式链路追踪需要集成Micrometer。更多详情可以参阅Micrometer官网 本文将以Spring Boot 3.2.x和Spring Clo…

HTML5球体下落粒子爆炸特效

HTML5球体下落粒子爆炸特效&#xff0c;源码由HTMLCSSJS组成&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面 下载地址 HTML5球体下落粒子爆炸特效

抖音无水印视频关键词批量下载|视频下载工具

抖音无水印视频关键词批量下载操作说明 我们根据自己的需要开发了抖音视频批量下载工具&#xff0c;现在市面上的视频无水印工具只能通过单个视频链接进行提取&#xff0c;太不方便 所以我们延伸出了 不仅可以通过单个视频链接进行提取也可通过关键词进行视频搜索 进行批量和有…

TCP和UDP基础

tcp服务器及客户端链接 ucd服务器及客户端 机械臂控制 #include<myhead.h> #define SER_PORT 8888 #define SER_IP "192.168.117.92" int main(int argc, const char *argv[]) { //创建套接字 int cfdsocket(AF_INET,SOCK_STREAM,0); if(cfd-1…

Python环境下基于1D-CNN、2D-CNN和LSTM的一维信号分类

以简单的西储大学轴承数据集为例&#xff0c;随便你下载几个信号玩耍吧&#xff0c;我选了10个信号&#xff0c;分别求为正常状态&#xff0c;内圈&#xff08;轻、中和重度损伤&#xff09;&#xff0c;外圈&#xff08;轻、中和重度损伤&#xff09;&#xff0c;滚动体&#…

鼎阳SDS6204示波器EPICS IOC的搭建

三年前曾写过这个文&#xff1a; 鼎阳SDS6204示波器的EPICS IOC调试 文章里有EPICS网站设备IOC搭建的指南&#xff0c;具体搭建IOC的步骤就没详细写了&#xff0c;几年后重新搭建时发现还是费了些力气才搭建起来&#xff0c;因此写此文记录下手把手的过程方便自己以及EPICS的初…

Matlab|基于改进粒子群算法的混合储能系统容量优化

目录 主要内容 部分程序 下载链接 主要内容 为了调高风光互补发电储能系统的经济性&#xff0c;减少其运行费用&#xff0c;研究风光互补发电储能系统的容量优化配置模型&#xff0c;探讨粒子群算法的改进及混合储能容量优化方法。首先通过对全生命周期费用静态模型…

【Sass】1px分割线 + 缩进分割线

效果图 1. 亮色模式效果 2. 暗色模式效果 设计思路 配色使用grey色 优点&#xff1a;无论在暗色模式还是亮色模式都可以看清楚分割线 使用after,before 伪元素绘制线条&#xff0c;并压缩线条transform: scaleY(.25) 注意事项 必须确保父级有宽高父级定位必须为position: r…