mybatis-plus 的saveBatch性能分析

news2025/6/20 11:36:07

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)
    @Override
    public 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 {
    @Override
    public 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 {
    @Bean
    public 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下面这段代码

 @Override
    public 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 statements
            int 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 needed
                                    checkCancelTimeout();
                                }

                                // limit one generated key per OnDuplicateKey statement
                                getBatchedGeneratedKeys(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.coloradmin.cn/o/1526649.html

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

相关文章

从历年315曝光案例,看APP隐私合规安全

更多网络安全干货内容&#xff1a;点此获取 ——————— 随着移动互联网新兴技术的发展与普及&#xff0c;移动APP的应用渗透到人们的衣食住行方方面面&#xff0c;衍生出各类消费场景的同时&#xff0c;也带来了无数的个人隐私数据泄露、网络诈骗事件。 历年来&#xff…

29.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-数据推测功能的算法实现

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;28.数据推测结果…

修改/etc/resolve.conf重启NetworkManager之后自动还原

我ping 百度报错&#xff1a; [rootk8snode1 ~]# ping baidu.com ping: baidu.com: Name or service not known很明显&#xff0c;这是DNS解析问题。 于是我修改 /etc/resolv.conf 文件后&#xff0c;执行完sudo systemctl restart NetworkManager&#xff0c;/etc/resolv.con…

Linux-多线程2 ——线程等待、线程异常、线程退出、线程取消和线程分离

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、线程间的全局变量共享__thread 修饰全局变量 二、线程等待pthread_self和tid 三、线程异常四、线程退出五、线程取消六、线程分离 一、线程间的全局变量共享 上…

ttkbootstrap界面美化系列之主窗口(二)

一&#xff1a;创建主窗口 在利用ttkbootstrap构建应用程序时&#xff0c;可以用tkinter传统的tk方法来创建主界面&#xff0c;也可以用ttkbootstrap中的window类来创建&#xff0c;下面我们来看看两者的区别 1&#xff0c;传统方法创建主界面 import tkinter as tk import …

力扣思路题:最长特殊序列1

int findLUSlength(char * a, char * b){int alenstrlen(a),blenstrlen(b);if (strcmp(a,b)0)return -1;return alen>blen?alen:blen; }

[CVPR-24] Text-to-3D using Gaussian Splatting

3DGS对初始化敏感&#xff1b;引入基于Point-E的3D SDS可以缓解多脸问题&#xff1b;外观细化阶段可以有效抑制异常点&#xff0c;并提高可视化效果&#xff1b;不需要对SDS的改进&#xff0c;用gudiance scale100可以取得很不错的结果。 [pdf | proj | code] 方法 Geometry O…

Linux——动静态库的制作及使用与动态库原理

目录 一、静态库 1.静态库的制作 2.静态库的使用 加载静态库方法一&#xff1a;安装头文件与库文件 加载静态库方法二&#xff1a;指定文件目录 二、动态库 1.动态库的制作 2.动态库的使用 方法一&#xff1a;安装到系统中 方法二&#xff1a;软链接 方法三&…

c语言文件操作(中)

目录 1. 文件的顺序读写1.1 顺序读写函数1.2 顺序读写函数的原型和介绍 结语 1. 文件的顺序读写 1.1 顺序读写函数 函数名功能适用于fgetc字符输入函数所有输出流fputc字符输出函数所有输出流fgets文本行输入函数所有输出流fputs文本行输出函数所有输出流fscanf格式化输入函数…

刷题DAY24 | LeetCode 77-组合

1 回溯法理论基础 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。回溯是递归的副产品&#xff0c;只要有递归就会有回溯。 所以以下讲解中&#xff0c;回溯函数也就是递归函数&#xff0c;指的都是一个函数。 1.1 回溯法的效率 回溯法的性能如何呢&#xff0…

完整指南:如何使用 Stable Diffusion API

Stable Diffusion 是一个先进的深度学习模型&#xff0c;用于创造和修改图像。这个模型能够基于文本描述来生成图像&#xff0c;让机器理解和实现用户的创意。使用这项技术的关键在于掌握其 API&#xff0c;通过编程来操控图像生成的过程。 在探索 Stable Diffusion API 的世界…

HarmonyOS NEXT应用开发之Web获取相机拍照图片案例

介绍 本示例介绍如何在HTML页面中拉起原生相机进行拍照&#xff0c;并获取返回的图片。 效果预览图 使用说明 点击HTML页面中的选择文件按钮&#xff0c;拉起原生相机进行拍照。完成拍照后&#xff0c;将图片在HTML的img标签中显示。 实现思路 添加Web组件&#xff0c;设置…

Vue.js+SpringBoot开发食品生产管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 加工厂管理模块2.2 客户管理模块2.3 食品管理模块2.4 生产销售订单管理模块2.5 系统管理模块2.6 其他管理模块 三、系统展示四、核心代码4.1 查询食品4.2 查询加工厂4.3 新增生产订单4.4 新增销售订单4.5 查询客户 五、…

生成式人工智能服务安全基本要求实务解析

本文尝试明晰《基本要求》的出台背景与实践定位&#xff0c;梳理《基本要求》所涉的各类安全要求&#xff0c;以便为相关企业遵循执行《基本要求》提供抓手。 引言 自2022年初以来&#xff0c;我国陆续发布算法推荐、深度合成与生成式人工智能服务相关的规范文件&#xff0c;…

阿里云服务器ECS经济型e实例性能如何?

阿里云服务器ECS推出经济型e系列&#xff0c;经济型e实例是阿里云面向个人开发者、学生、小微企业&#xff0c;在中小型网站建设、开发测试、轻量级应用等场景推出的全新入门级云服务器&#xff0c;CPU采用Intel Xeon Platinum架构处理器&#xff0c;支持1:1、1:2、1:4多种处理…

JS第一阶段1

文章目录 1. js组成2. JS三种书写位置JS输出语句 3. 变量4. 数据类型Number字符串型 String布尔型booleanUnddefined和Null 5. 获取变量的数据类型获取检测变量的数据类型 6. 数据转换类型转换为字符串转换为数字型&#xff08;重点&#xff09;转换为布尔型 7.运算符算数运算符…

找不到msvcp110.dll怎么办,msvcp110.dll丢失的5种修复方法

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcp110.dll丢失”。由于msvcp110.dll是Microsoft Visual C Redistributable Package的重要组成部分&#xff0c;它的缺失会导致依赖于该组件的软件无法正常启动或运行&#xff0c;比如某…

从初学者到专家:Java反射的完整指南

一.反射的概念及定义 Java 的反射&#xff08; reflection &#xff09;机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意方法和属性&#xff0c;既然能拿到那么&#x…

Jenkins + Docker + ASP.NET Core自动化部署

本来没想着要写这篇博客&#xff0c;但是在实操过程中&#xff0c;一个是被网络问题搞炸了心态&#xff08;真心感觉网络能把人搞疯&#xff0c;别人下个包、下个镜像几秒钟搞定&#xff0c;我看着我的几KB小水管真是有苦说不出&#xff09;&#xff0c;另一个就是这里面坑还是…

C语言---指针的两个运算符:点和箭头

目录 点&#xff08;.&#xff09;运算符箭头&#xff08;->&#xff09;运算符需要注意实际例子 C语言中的指针是一种特殊的变量&#xff0c;它存储了一个内存地址。点&#xff08;.&#xff09;和箭头&#xff08;->&#xff09;是用于访问结构体和联合体成员的运算符。…