Mybatis-plus的批量插入真的不能用?开销真的很大?真的有去了解吗?

前言

在很多公司,可以从很多人口中听到“不要用 mybatis-plus 的批量插入,它其实也是遍历插入,性能很差的”。真的是这样吗?我们不应该人云亦云,应该自己去看下。
我们先针对这个观点分析下,总结出来他们的看法应该是下面的其中之一:

  1. 遍历插入,反复创建 Connection,众所周知这是一个比较重的操作,所以性能很差。
    这里不用看源码应该也能知道,因为这个和mybatis-plus没关系,和连接有关系,连接池就是为了支持连接复用出现的。连接和连接池不是本章节的重点,就不展开讲了,总的来说这观点是不正确的。
  2. 一条 insert 就一次网络IO,数量多了,这是个很可观且没必要的开销,所以性能差。

走进源码

对这第二个观点,笔者结合源码给出自己的观点

下面给出笔者的一些相关配置(我mybatis-plus的版本是3.5.3.1
pom.xml如下

...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
...

application.yml如下

spring:
  # 使用默认的连接池库
  datasource:
    url: "jdbc:mysql://***/***?useSSL=false&useUnicode=true&characterEncoding=utf8&ApplicationName=spring-boot-demo&serverTimezone=UTC&allowMultiQueries=true"
    username: "****"
    password: "****"

service如下图


image.png

1 进入 saveBatch 看下

com.baomidou.mybatisplus.extension.service.IService#saveBatch(java.util.Collection<T>)

image.png

发现里面会给我们这个批量操作开启了事务(如果是期望插入一条就成功一条的,这批量方法就不适用了)并且是有限制提交数量的,默认1000。

2 往里ServiceImpl#saveBatch走

com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch

image.png

看到了 SqlMethod.INSERT_ONE,很多人都知道了是 mybatis-plus 的批量插入是一条条插入的,但是这个一次次的遍历是真的发送给MySQL了吗?这里留一个疑问。
我们只要记得这里有一个钩子,后面会回调回来执行 sqlSession.insert(sqlStatement, entity)。

3 SqlHelper#executeBatch(Class<?>, Log, Collection<E>, int, BiConsumer<SqlSession,E>)

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>)

image.png

这里的 sqlSession 是 org.apache.ibatis.session.SqlSession,mybatis 自己定义的类。
我们去看下截图里面的executeBatch

4 SqlHelper#executeBatch(Class<?> entityClass, Log log, Consumer<SqlSession> consumer)

com.baomidou.mybatisplus.extension.toolkit.SqlHelper#executeBatch(java.lang.Class<?>, org.apache.ibatis.logging.Log, java.util.function.Consumer<org.apache.ibatis.session.SqlSession>)

image.png

结合前面的代码就知道了,我这里是到了1000(默认配置1000,并且我批量保存的list超过了1000),就会将内存的sql全部刷到MySQL,然后回去继续遍历,全部遍历完后就会 close。

测试

实践出真知

笔者知道有些同学在实际工作中试过,发现其实 mybatis-plus 的批量插入其实比自己写 xml 的慢很多。这里面涉及的原因蛮多的。先看成功案例,然后再说原因。
代码,配置,版本还是之前说的,这里需要补充下笔者的 mysql 版本和驱动版本:

  • mysql
    5.7.25
  • 驱动


    驱动

    测试代码如下图


    单元测试
@Slf4j
@Service
@RequiredArgsConstructor
public class WaterNumService extends ServiceImpl<WaterNumMapper, WaterNumEntity> {
    private final WaterGeneratorService waterGeneratorService;

    @Transactional(rollbackFor = Exception.class)
    public void generateWaterNum(GenerateWaterNumRequest req) {
        WaterNumGenerateDTO param = WaterNumGenerateDTO.builder()
                .group(req.getGroup())
                .prefix(req.getPrefix())
                .generateNum(req.getGenerateNum())
                .maxNum(req.getMaxNum())
                .fixLen(req.getFixLen())
                .offset(req.getOffset())
                .build();
        List<String> waterNumList = waterGeneratorService.generateWaterNumBatch(param);
        List<WaterNumEntity> entityList = waterNumList.stream().map(waterNum -> {
            WaterNumEntity entity = new WaterNumEntity();
            entity.setRuleGroup(req.getGroup());
            entity.setWaterNum(waterNum);
            return entity;
        }).collect(Collectors.toList());
        long begin = System.currentTimeMillis();
        // mybatis-plus 的批量插入
        saveBatch(entityList);
        log.debug("generateWaterNum#批量插入耗时 : {}ms", System.currentTimeMillis() - begin);
    }
}

运行结果,如下图


运行结果

当然,这个速度和我插入的表,还有我插入的字段的数量,大字段的数量有关系,还有数据库所在的服务器的磁盘也有关系,10000也不算特别多,但是整体上看其实也还能接受的。

那么为什么有些人在工作中也是1W左右的批量插入就特别慢呢?(参考资料 : https://github.com/baomidou/mybatis-plus/issues/2786)
这是因为 mysql-plus 的批量插入对 mysql 的版本和驱动的版本有关。在较旧版本的驱动默认下会将我们期望的一组批量执行的 sql 拆开发送,可以尝试增加参数 rewriteBatchedStatements,具体如下

spring:
  # 使用默认的连接池库
  datasource:
    url: "jdbc:mysql://***/***?useSSL=false&useUnicode=true&characterEncoding=utf8&ApplicationName=spring-boot-demo&serverTimezone=UTC&allowMultiQueries=true&rewriteBatchedStatements=true"
    username: "****"
    password: "****"

rewriteBatchedStatements=true 结合 5.1.13 以上版本的驱动,这样 statement 才会真正执行 executeBatch()。
增加参数后的运行时间如下


image.png

总结

到这里大家应该都清楚的知道了 mybatis-plus 的批量插入虽然是遍历插入,但是不是一个insert就一次io,而是多条insert打包在一起分批发送的,所以性能不会有什么太大问题。不过笔者这里不是鼓吹大家都用这个批量插入就好了,实际工作中会有更多要求,其实这个简单的批量插入是没法满足的。因此,笔者只是提倡可以根据自己工作实际情况决定,但是性能方面就不用太过担心,mybatis-plus也有考虑的,详情请看:通用 insertBatch 为什么放在 service 层处理

扩展

如果使用 mybatis-plus 3.4+ 版本,并且连接的是 MySQL 8.0 或更高版本的数据库,那么 mybatis-plus 将会自动利用 MySQL 8.0 的原生批量插入功能来执行批量插入操作。

具体实现的关键是在 mybatis-plus 的底层使用了 JDBC 的 addBatch 和 executeBatch 方法。当调用 mybatis-plus 的批量新增时,mybatis-plus 会将待插入的对象列表传递给底层的 JDBC 驱动程序。而 MySQL 8.0 的 JDBC 驱动程序会自动将这些插入语句封装成批量插入的 SQL 语句,并一次性发送给数据库执行。

需要注意的是,要确保以下条件满足才能利用 MySQL 8.0 的批量插入功能:

  1. 使用 MySQL 8.0 或更高版本的数据库
  2. 使用兼容 MySQL 8.0 的 JDBC 驱动程序(如 mysql-connector-java 版本 8.0 或更高)
  3. 使用 mybatis-plus 3.4+ 版本
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容