Mybatis批量插入的正确姿势到底是什么?在网上浏览了非常多的帖子,很多都是复制粘贴来的,内容基本都是在误导别人,几乎没有测试验证,如果照做的话,性能反而相对于单条几乎没有任何提升,实践才是检验真理的唯一标准。参差不齐的网络文章真的很让人生气,完全是对技术的不负责任。
SpringBoot项目的基本目录如下:
不再详细的介绍搭建项目了,项目的搭建要求是能够通过Mybatis对mysql数据库进行CRUD操作即可,其他功能不需要具备。主要重心放在批量插入方式的讨论与插入性能的对比测试。
application.yml:
server:
port: 20100
spring:
application:
name: demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/test?useSSL=false&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
mybatis:
mybatis.type-aliases-package: com.example.demo.entity
mapper-locations: classpath:/mapper/**/*.xml
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
User实体类:
package com.example.demo.entity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* @Author: zongshaofeng
* @Description:
* @Date:Create:in 2021/9/12 11:52
* @Modified By:
*/
@Data
public class User implements Serializable {
private Integer id;
private Date time;
private String name;
private String content;
}
UserMapper:
package com.example.demo.dao;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author: zongshaofeng
* @Description:
* @Date:Create:in 2021/9/12 11:55
* @Modified By:
*/
@Mapper
@Repository
public interface UserMapper {
List<User> listUser();
Boolean insertUser(@Param("user")User user);
Boolean insertUserList(@Param("userList")List<User> userList);
}
UserMapper.xml:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserMapper">
<insert id="insertUser">
insert into user (time,name,content)
values
(
#{user.time},#{user.name},#{user.content}
)
insert>
<insert id="insertUserList">
insert into user (time,name,content)
values
<foreach collection="userList" item="user" index="index" separator=",">
(#{user.time},#{user.name},#{user.content})
foreach>
insert>
<select id="listUser" resultType="com.example.demo.entity.User">
select * from user
select>
mapper>
数据表建表语句:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`time` datetime DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`content` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4;
这个是使用for循环,循环单条插入数据库,每一次循环伴随SqlSession的打开和关闭都是一个单独的事务提交,性能最差,这里使用这种方式来作为对比标准,看看批量的方式能够提高多少性能。
在测试类中编写测试方法如下:
@Test
void insertForUser() {
Instant start=Instant.now();
try {
int count = 10000;
for (int i = 0; i < count; i++) {
User user = new User();
user.setTime(new Date());
user.setName("单条插入" + i);
user.setContent("这是通过for循环单条插入的一条记录");
userMapper.insertUser(user);
}
} catch (Exception exception) {
exception.printStackTrace();
}
System.out.println("***********************for循环单条写入总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
}
这个是使用mapper.xml文件中foreach循环,批量插入数据库,其本质就是将所有的数据组成一条sql语句,在一次操作中发送给数据库进行执行。但是你以为这种方式就最佳实践了吗?sql语句的大小数据库是有限制的,mysql限制为1M啊,下面测试时会提到,如果一口气插入10万条数据试一下,不用10万,5万都会抛出语句过大异常。
在测试类中编写测试方法如下:
@Test
void insertForeachUser() {
Instant start=Instant.now();
try {
int count =10000;
List<User> userList=new ArrayList<>();
for (int i = 0; i < count; i++) {
User user = new User();
user.setTime(new Date());
user.setName("批量插入" + i);
user.setContent("这是通过foreach批量插入的一条记录");
userList.add(user);
}
userMapper.insertUserList(userList);
} catch (Exception exception) {
exception.printStackTrace();
}
System.out.println("***********************foreach批量写入总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
}
Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优。
上面这段话我复制的,所有帖子都会写这句话,真的性能将更优吗???放开Mybatis日志输出,可以发现batch模式下确实重复使用已经预处理的语句,只是将动态更新parameters,在最终的commit之前数据库中确实没有插入数据,但是性能与for循环一次次插入几乎没有变化,下面我都将进行测试,稍安勿躁。
在测试类中编写测试方法如下:
再次声明,下面的这个测试方法,是网上非常多的帖子给出的大幅度提高插入性能的batch方式采用的方法,我严重怀疑都是复制粘贴的。我类比过来的几乎没改动,等会测一测试试嘛!
@Test
void insertBatchUserOne() {
Instant start=Instant.now();
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
try {
int count = 100000;
for (int i = 0; i < count; i++) {
User user = new User();
user.setTime(new Date());
user.setName("批量插入" + i);
user.setContent("这是通过batch批量插入的一条记录");
mapper.insertUser(user);
if (i % 1000 == 0 || i == count - 1) {
sqlSession.commit();
sqlSession.clearCache();
}
}
} catch (Exception exception) {
sqlSession.rollback();
} finally {
sqlSession.close();
}
System.out.println("***********************batch总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
}
就是将3和4集合起来而已。
在测试类中编写测试方法如下:
@Test
void insertBatchUserTwo() {
Instant start=Instant.now();
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
try {
int count = 10000;
List<User> userList=new ArrayList<>();
for (int i = 0; i < count; i++) {
User user = new User();
user.setTime(new Date());
user.setName("批量插入" + i);
user.setContent("这是通过batch+foreach批量插入的一条记录");
userList.add(user);
if (i % 1000 == 0 || i == count - 1) {
mapper.insertUserList(userList);
sqlSession.commit();
sqlSession.clearCache();
userList.clear();
}
}
} catch (Exception exception) {
sqlSession.rollback();
} finally {
sqlSession.close();
}
System.out.println("***********************batch+foreach总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
}
为了保证测试结果的相对准确性,同一种方法不同数据量的测试依次执行,比如for循环,先插入1000,记录时间,再次运行插入10000,记录时间,最终走完所有的情况后,数据库表中数据为111000条数据。然后在测试下一种方式时,首先truncate user,将表数据清零。
插入数据量 | for循环单条插入 | foreach批量插入 | 单独batch批量插入 | batch+foreach批量插入 |
---|---|---|---|---|
1000(1千) | 1498毫秒 | 445毫秒 | 976毫秒 | 434毫秒 |
10000(1万) | 8390毫秒 | 860毫秒 | 6853毫秒 | 810毫秒 |
100000(10万) | 67457毫秒 | Exception:Packet for query is too large | 57985毫秒 | 4052毫秒 |
1000000(100万) | 652247毫秒 | 测试无意义 | 574642毫秒 | 21676毫秒 |
看到上述的测试结果,不言自明了吧,像网上大多数帖子说的那样,仅仅使用batch,性能比for循环一条条插入快不到哪里去,验证了那一句,不服跑个分?
单纯的foreach拼装sql语句有大小限制。
batch可以解决sql语句大小限制的问题,但就像阴晴圆圈、悲欢离合,一切事物都有两面性,batch也有其自身的缺点,这时候就有了那一句:不存在的完美,适合自己才是最好的。
最后,本文并没有描述任何理论的东西,如果感兴趣,请继续查阅资料学习吧。