在项目开发中,我们经常需要进行大量数据的批量插入操作。
然而,在实际应用中,插入大量数据时性能常常成为一个瓶颈。
在不念最近的项目中,我发现了一些能够显著提升批量插入性能的方法,并进行了一系列实验来验证它们的有效性。
背景介绍
我们使用了 mybatis-plus 框架,并采用其中的 saveBatch 方法进行批量数据插入。
然而,通过深入研究源码,我发现这个方法并没有如我期望的那样高效
这是因为最终在执行的时候还是通过for循环一条条执行insert,然后再一批的进行flush ,默认批的消息为1000
为了找到更优秀的解决方案,我展开了一场性能优化的探索之旅。好了我们现在开始探索
实验准备
- 创建一张表tb_student
create table springboot_mp.tb_student
(
id bigint auto_increment comment '主键ID'
primary key,
stuid varchar(40) not null comment '学号',
name varchar(30) null comment '姓名',
age tinyint null comment '年龄',
sex tinyint(1) null comment '性别 0 男 1 女',
dept varchar(2000) null comment '院系',
address varchar(400) null comment '家庭地址',
constraint stuid
unique (stuid)
);
- 创建
spring-boot-mybatis-demo
项目并在pom.xml中添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
- application.yml配置
server:
port: 8890
spring:
application:
name: mybatis-demo #指定服务名
datasource:
username: root
password: root
# url: jdbc:mysql://localhost:3306/springboot_mp?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
url: jdbc:mysql://localhost:3306/springboot_mp?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
#
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
- 使用mybatisX生成代码
探索实验
每次都是插入100000条数据
注意:因为我的电脑性能比较好,所以才插入这么多数据,大家可以插入1000进行实验对比
- 单条循环插入:传统方法的基准
首先,我采用了传统的单条循环插入方法,将每条数据逐一插入数据库,作为性能对比的基准。
/**
* @author springboot
* @description: TODO
*/
@SpringBootTest
public class MybatisTest {
@Autowired
private StudentService studentService;
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
public void MybatisBatchSaveOneByOne(){
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start("mybatis plus save one");
for (int i = 0; i < 100000; i++) {
Student student = new Student();
student.setStuid("6840"+i);
student.setName("zhangsan"+i);
student.setAge((i%100));
if(i%2==0){
student.setSex(0);
}else {
student.setSex(1);
}
student.setDept("计算机学院");
student.setAddress("广东省广州市番禺"+i+"号");
//一条一条插入
studentService.save(student);
}
sqlSession.commit();
stopWatch.stop();
System.out.println("mybatis plus save one:" + stopWatch.getTotalTimeMillis());
} finally {
sqlSession.close();
}
}
}
发现花费了195569毫秒
- mybatis-plus 的 saveBatch 方法
现在尝试 mybatis-plus 提供的 saveBatch 方法,期望它能够提高性能。
@Test
public void MybatissaveBatch(){
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<Student> students = new ArrayList<>();
StopWatch stopWatch = new StopWatch();
stopWatch.start("mybatis plus save batch");
for (int i = 0; i < 100000; i++) {
Student student = new Student();
student.setStuid("6840"+i);
student.setName("zhangsan"+i);
student.setAge((i%100));
if(i%2==0){
student.setSex(0);
}else {
student.setSex(1);
}
student.setDept("计算机学院");
student.setAddress("广东省广州市番禺"+i+"号");
//一条一条插入
students.add(student);
}
studentService.saveBatch(students);
sqlSession.commit();
stopWatch.stop();
System.out.println("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());
} finally {
sqlSession.close();
}
}
发现花费9204毫秒,比一条条插入数据性能提高十几倍
- 手动拼接 SQL:挑战传统的方式
<insert id="saveBatch2">
insert into springboot_mp.tb_student ( stuid, name, age, sex, dept, address)
values
<foreach collection="students" item="stu" index="index" separator=",">
( #{stu.stuid}, #{stu.name}, #{stu.age}, #{stu.sex}, #{stu.dept}, #{stu.address})
</foreach>
</insert>
发现花费10958毫秒,比一条条插入数据性能提高十几倍,但是和saveBatch性能相差不大
既然都验证都这了,我就在想,要不要使用JDBC批量插入进行验证一下,看会不会出现原始的才是最好的结果
- JDBC 的 executeBatch 方法
尝试直接使用 JDBC 提供的 executeBatch 方法,看是否有意外的性能提升。
@Test
public void JDBCSaveBatch() throws SQLException {
SqlSession sqlSession = sqlSessionFactory.openSession();
Connection connection = sqlSession.getConnection();
connection.setAutoCommit(false);
String sql ="insert into springboot_mp.tb_student ( stuid, name, age, sex, dept, address) values (?, ?, ?, ?, ?, ?);";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
List<Student> students = new ArrayList<>();
StopWatch stopWatch = new StopWatch();
stopWatch.start("mybatis plus JDBCSaveBatch");
for (int i = 0; i < 100000; i++) {
statement.setString(1,"6840"+i);
statement.setString(2,"zhangsan"+i);
statement.setInt(3,(i%100));
if(i%2==0){
statement.setInt(4,0);
}else {
statement.setInt(4,1);
}
statement.setString(5,"计算机学院");
statement.setString(6,"广东省广州市番禺"+i+"号");
statement.addBatch();
}
statement.executeBatch();
connection.commit();
stopWatch.stop();
System.out.println("mybatis plus JDBCSaveBatch:" + stopWatch.getTotalTimeMillis());
}
catch (Exception e){
System.out.println(e.getMessage());
}
finally {
sqlSession.close();
}
JDBC executeBatch 的性能会好点,耗费6667毫秒
但是感觉到这里以后,觉得时候还是比较长,有没有可以再进行优化的方式,然后我就在ClientPreparedStatement
类中发现有一个叫做rewriteBatchedStatements
的属性,从名字来看是要重写批操作的 Statement,前面batchHasPlainStatements 已经是 false,取反肯定是 true,所以只要这参数是 true 就会进行一波操作。rewriteBatchedStatements默认是 false。
大家也可以自行网上搜索一下这个神奇的属性然后我在url添加上这个属性
然后继续跑了下 mybatis-plus 自带的 saveBatch,果然性能大大提高直接由原来的9204毫秒,提升到现在的3903毫秒
再来跑一下JDBC 的 executeBatch ,果然也提高了。
直接由原来的6667毫秒,提升到了3794毫秒
结果对比
批量保存方式 | 数据量(条) | 耗时(ms) |
---|---|---|
单条循环插入 | 100000 | 195569 |
mybatis-plus saveBatch | 100000 | 9204 |
mybatis-plus saveBatch(添加 rewrite 参数) | 100000 | 3903 |
手动拼接 SQL | 100000 | 6667 |
JDBC executeBatch | 100000 | 10958 |
JDBC executeBatch(添加 rewrite 参数) | 100000 | 3794 |
总结
通过实验结果,我们可以得出以下结论:
- mybatis-plus 的 saveBatch 方法相比单条循环插入在性能上有所提升,但仍然不够理想。
- JDBC 的 executeBatch 方法在默认情况下性能与 mybatis-plus 的 saveBatch 类似,但通过设置 rewriteBatchedStatements 参数为 true 可显著提高性能。
- rewriteBatchedStatements 参数的作用是将一批插入拼接成 insert into xxx values (a),(b),(c)… 这样的一条语句形式,提高了性能。
优化建议
如果您在项目中需要进行批量插入操作,我建议考虑以下优化方案:
- 如果使用 mybatis-plus,可以尝试将 JDBC 连接字符串中的 rewriteBatchedStatements 参数设置为 true,以提高 saveBatch 方法的性能。