前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mybatis-plus批量插入你用对了吗

mybatis-plus批量插入你用对了吗

原创
作者头像
阿建dong一点
修改2022-11-26 14:40:40
2.7K1
修改2022-11-26 14:40:40
举报
文章被收录于专栏:写点吧写点吧

一次代码review,大佬说了算

记得有一次我们小组code review,组长看了下我们批量插入是使用mybatis原生的xml foreach实现的,于是二话不说,拍桌子,说这有性能问题。叫我们直接使用mybatis-plus,可是为啥呢?怎么用,需要注意哪些地方,也没给我们说个明白。好吧,我们对这一块也没具体调研过,就直接按他的想法去实现了。性能有没有提升了好几倍呢,其实也没实践过,反正review过了。直到有一天。。。

mybatis-plus批量插入就一定比mybatis原生foreach强?

实践是检验真理的唯一标准,我们分别使用mp批量插入方法和mybatis foreach来验证

相关环境准备

引入 mybatis-plus

代码语言:txt
复制
   <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

mapper及对应xml

代码语言:txt
复制
public interface UserMapper extends BaseMapper<UserInfo> {
    /**
     * 原生批量插入
     * @param list
     * @return
     */
    int saveBatchByNative(@Param("list") List<UserInfo> list);
}
代码语言:txt
复制
    <insert id="saveBatchByNative" >
        INSERT INTO `t_user`(`name`,`age`,`descr`) VALUES
        <foreach collection="list" separator="," item="item">
            (#{item.name},#{item.age},#{item.descr})
        </foreach>
    </insert>

按照惯例,controller及service 调用

代码语言:txt
复制
@Service("mybatisUserService")
public class UserService extends ServiceImpl<UserMapper,UserInfo> {

    @Autowired
    private UserMapper userMapper;

    @Transactional(rollbackFor = Exception.class)
    public void insertBatchByPlus(int maxInsert){
        List<UserInfo> users = getUsers(maxInsert);
        long start = System.currentTimeMillis();
        boolean insert = this.saveBatch(users,1000);
        System.out.println("mp batch insert row :"+maxInsert+" and spend time(ms) :"+(System.currentTimeMillis()-start));

    }

    @Transactional(rollbackFor = Exception.class)
    public void insertBatchByNative(int maxInsert){
        List<UserInfo> users = getUsers(maxInsert);
        long start = System.currentTimeMillis();
        int insert = userMapper.saveBatchByNative(users);
        System.out.println("native batch insert row :"+insert+" and spend time(ms) :"+(System.currentTimeMillis()-start));

    }



@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/insert")
    public void addUser(@RequestParam(name = "max") int max){
//        userService.insert();

        userService.insertBatchByNative(max);

        userService.insertBatchByPlus(max);
    }

}

测试结果

分别测 10000 、50000,执行结果截图

image.png
image.png
image.png
image.png

意外吧!基于前面的环境,mybatis-plus并没有占优势,反而慢得离谱。难道大佬是瞎说的?其实也不全是。。

开启批量插入

数据源配置url参数加上 rewriteBatchedStatements=true,如

代码语言:txt
复制
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/study_01?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=xxxxx

再次测试结果

还是一样,分别测试10000、50000 ,执行结果截图

image.png
image.png

咋一看,mybatis并没有差太多。于是,再调下,mp 的batchsize成2000(前面设置了1000)

image.png
image.png

这不太对,并非越大越好。于是,调低 ,batchsize = 200

image.png
image.png

是有一丢丢优势,不过,也并非batchsize越小越好,因为越小不就是和one by one 差不多了吗

难道,开了批量参数rewriteBatchedStatements,mp就一定比mybatis快了?也并非如此,看下

image.png
image.png

可能遇到的问题

  1. 关于max_allowed_packet的配置,异常表示当前发送的包太大了
image.png
image.png

可通过my.cnf 或者 set global max_allowed_packet = 10485760 配置,它们的区别在于重启之后配置的有效性

粗略看下执行流程

mybatis-plus 批量实现

代码语言:txt
复制
  @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = sqlStatement(SqlMethod.INSERT_ONE);
        try (SqlSession batchSqlSession = sqlSessionBatch()) {
            int i = 0;
            for (T anEntityList : entityList) {
                batchSqlSession.insert(sqlStatement, anEntityList);
                if (i >= 1 && i % batchSize == 0) {
                    //按我们传入的batchsize 分批插入
                    //不过,是否真的分批还得往下看,也有可能一种假象
                    batchSqlSession.flushStatements();
                }
                i++;
            }
            batchSqlSession.flushStatements();
        }
        return true;
    }

最终走到 com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchInternal

代码语言:txt
复制
@Override
    protected long[] executeBatchInternal() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            //省略一些无关代码

            try {
                //省略一些无关代码
                // batchHasPlainStatements 默认为false 
                if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {

                    if (((PreparedQuery<?>) this.query).getParseInfo().canRewriteAsMultiValueInsertAtSqlLevel()) {
                        return executeBatchedInserts(batchTimeout);
                    }

                    if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
                            && this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
                        return executePreparedBatchAsMultiStatement(batchTimeout);
                    }
                }
                //如果不开启 rewriteBatchedStatements 则走这个逻辑
                return executeBatchSerially(batchTimeout);
            } finally {
                this.query.getStatementExecuting().set(false);

                clearBatch();
            }
        }
    }

先看下executeBatchSerially 方法,作者对这个方法的解释是

image.png
image.png
代码语言:txt
复制
 protected long[] executeBatchSerially(int batchTimeout) throws SQLException {

        synchronized (checkClosed().getConnectionMutex()) {
          
              // 省略一些无关代码

              //nbrCommands 其实就是 batchsize,
              // 所以对于该方法,本质就是通过遍历,一条一条插入。

                    for (batchCommandIndex = 0; batchCommandIndex < nbrCommands; batchCommandIndex++) {

                          // 省略一些无关代码

                        // 关注下 executeUpdateInternal 这个方法很重要,可以理解为一次跟server端交互对sql的执行过程
>) arg;
                                updateCounts[batchCommandIndex] = executeUpdateInternal(queryBindings, true);

                              // 省略一些无关代码

                 
                    }

            return (updateCounts != null) ? updateCounts : new long[0];
        }
    }

再看下com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchedInserts,同样,看下解释

image.png
image.png
代码语言:txt
复制
protected long[] executeBatchedInserts(int batchTimeout) throws SQLException {

           // 省略一些无关代码

                     for (int i = 0; i < numberArgsToExecute; i++) {

                       // numValuesPerBatch == numberArgsToExecute 就不会满足
                        if (i != 0 && i % numValuesPerBatch == 0) {
                            //一般不会执行到这里,除非批量执行sql 的 size 超过了 max_allowed_packet
                            try {
                                updateCountRunningTotal += batchedStatement.executeLargeUpdate();
                            } catch (SQLException ex) {
                                sqlEx = handleExceptionForBatch(batchCounter - 1, numValuesPerBatch, updateCounts, ex);
                            }

                            getBatchedGeneratedKeys(batchedStatement);
                            batchedStatement.clearParameters();
                            batchedParamIndex = 1;

                        }
                        //通过遍历给每个sql进行参数设置
                        batchedParamIndex = setOneBatchedParameterSet(batchedStatement, batchedParamIndex, this.query.getBatchedArgs().get(batchCounter++));
                    }

                    try {
                        //执行批量插入,其实也就是调用了executeUpdateInternal方法
                        updateCountRunningTotal += batchedStatement.executeLargeUpdate();
                    } catch (SQLException ex) {
                        sqlEx = handleExceptionForBatch(batchCounter - 1, numValuesPerBatch, updateCounts, ex);
                    }

       // 省略一些无关代码

}



   /**
     * JDBC 4.2
     * Same as PreparedStatement.executeUpdate() but returns long instead of int.
     */
    public long executeLargeUpdate() throws SQLException {
        return executeUpdateInternal(true, false);
    }

可以看出 executeBatchSerially和executeBatchedInserts 最终都调用 executeUpdateInternal方法(可以简单的理解为jdbc和server服务端一次通信的过程),区别在于是否通过遍历一条一条的发送;

mybatis foreach 执行过程

由于引入的mybatis-plus ,mapper代理类是MybatisMapperProxy

代码语言:txt
复制
   @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (method.isDefault()) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

最终走到com.mysql.cj.jdbc.ClientPreparedStatement#execute

代码语言:txt
复制
 public boolean execute() throws SQLException {
// 省略一些无关代码

Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();

// 一次打包发送到server端
  rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(),
                    (((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S'), cachedMetadata, false);

// 省略一些无关代码

 }

可以看出,对于mybatis foreach 批量插入,直接就是一次交互过程,跟是否开启rewriteBatchedStatements无关,唯一有关的就是max_allowed_packet的设置

最后

  1. mybatis原生foreach批量插入,抛开网络阻塞问题,其耗时并非想的差劲。但是,为何在大数据量批量插入场景下不推荐使用,可能就是考虑网络阻塞以及server端处理涉及到长事务问题吧;
  2. 使用mybatis-plus 批量插入,需要开启rewriteBatchedStatements,且合理设置batchsize,该参数会影响客户端与服务端通信交互次数。如果batchsize太大,超过最大packet,jdbc底层还是会再一次分批,反而影响性能;
  3. 批量操作用原生还是mybatis-plus?我觉得应该有个范围,比如实际场景一次批量插入最多也就几百条且size也比较小,那使用原生的问题也不大

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一次代码review,大佬说了算
  • mybatis-plus批量插入就一定比mybatis原生foreach强?
    • 相关环境准备
      • 测试结果
        • 开启批量插入
          • 再次测试结果
            • 可能遇到的问题
            • 粗略看下执行流程
              • mybatis-plus 批量实现
                • mybatis foreach 执行过程
                • 最后
                相关产品与服务
                云数据库 SQL Server
                腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档