前言
在很多公司,可以从很多人口中听到“不要用 mybatis-plus 的批量插入,它其实也是遍历插入,性能很差的”。真的是这样吗?我们不应该人云亦云,应该自己去看下。
我们先针对这个观点分析下,总结出来他们的看法应该是下面的其中之一:
- 遍历插入,反复创建 Connection,众所周知这是一个比较重的操作,所以性能很差。
这里不用看源码应该也能知道,因为这个和mybatis-plus没关系,和连接有关系,连接池就是为了支持连接复用出现的。连接和连接池不是本章节的重点,就不展开讲了,总的来说这观点是不正确的。 - 一条 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如下图
1 进入 saveBatch 看下
com.baomidou.mybatisplus.extension.service.IService#saveBatch(java.util.Collection<T>)
发现里面会给我们这个批量操作开启了事务(如果是期望插入一条就成功一条的,这批量方法就不适用了)并且是有限制提交数量的,默认1000。
2 往里ServiceImpl#saveBatch走
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch
看到了 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>)
这里的 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>)
结合前面的代码就知道了,我这里是到了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()。
增加参数后的运行时间如下
总结
到这里大家应该都清楚的知道了 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 的批量插入功能:
- 使用 MySQL 8.0 或更高版本的数据库
- 使用兼容 MySQL 8.0 的 JDBC 驱动程序(如 mysql-connector-java 版本 8.0 或更高)
- 使用 mybatis-plus 3.4+ 版本