MyBatis批量插入數(shù)據(jù)優(yōu)化,那叫一個(gè)優(yōu)雅!
在項(xiàng)目開(kāi)發(fā)中,我們經(jīng)常需要進(jìn)行大量數(shù)據(jù)的批量插入操作。然而,在實(shí)際應(yīng)用中,插入大量數(shù)據(jù)時(shí)性能常常成為一個(gè)瓶頸。在我最近的項(xiàng)目中,我發(fā)現(xiàn)了一些能夠顯著提升批量插入性能的方法,并進(jìn)行了一系列實(shí)驗(yàn)來(lái)驗(yàn)證它們的有效性。
今日內(nèi)容介紹,大約花費(fèi)15分鐘
圖片
背景介紹
我們使用了 mybatis-plus 框架,并采用其中的 saveBatch 方法進(jìn)行批量數(shù)據(jù)插入。然而,通過(guò)深入研究源碼,我發(fā)現(xiàn)這個(gè)方法并沒(méi)有如我期望的那樣高效
圖片
這是因?yàn)樽罱K在執(zhí)行的時(shí)候還是通過(guò)for循環(huán)一條條執(zhí)行insert,然后再一批的進(jìn)行flush ,默認(rèn)批的消息為1000
圖片
為了找到更優(yōu)秀的解決方案,我展開(kāi)了一場(chǎng)性能優(yōu)化的探索之旅。好了我們現(xiàn)在開(kāi)始探索
實(shí)驗(yàn)準(zhǔn)備
- 創(chuàng)建一張表tb_student
create table springboot_mp.tb_student
(
id bigint auto_increment comment '主鍵ID'
primary key,
stuid varchar(40) not null comment '學(xué)號(hào)',
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)
);- 創(chuàng)建spring-boot-mybatis-demo項(xiàng)目并在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 #指定服務(wù)名
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生成代碼
圖片
圖片
圖片
探索實(shí)驗(yàn)
每次都是插入100000條數(shù)據(jù)
注意:因?yàn)槲业碾娔X性能比較好,所以才插入這么多數(shù)據(jù),大家可以插入1000進(jìn)行實(shí)驗(yàn)對(duì)比
- 單條循環(huán)插入:傳統(tǒng)方法的基準(zhǔn)
首先,我采用了傳統(tǒng)的單條循環(huán)插入方法,將每條數(shù)據(jù)逐一插入數(shù)據(jù)庫(kù),作為性能對(duì)比的基準(zhǔn)。
/**
* @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("計(jì)算機(jī)學(xué)院");
student.setAddress("廣東省廣州市番禺"+i+"號(hào)");
//一條一條插入
studentService.save(student);
}
sqlSession.commit();
stopWatch.stop();
System.out.println("mybatis plus save one:" + stopWatch.getTotalTimeMillis());
} finally {
sqlSession.close();
}
}
}發(fā)現(xiàn)花費(fèi)了195569毫秒
圖片
- mybatis-plus 的 saveBatch 方法
現(xiàn)在嘗試 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("計(jì)算機(jī)學(xué)院");
student.setAddress("廣東省廣州市番禺"+i+"號(hào)");
//一條一條插入
students.add(student);
}
studentService.saveBatch(students);
sqlSession.commit();
stopWatch.stop();
System.out.println("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());
} finally {
sqlSession.close();
}
}發(fā)現(xiàn)花費(fèi)9204毫秒,比一條條插入數(shù)據(jù)性能提高十幾倍

3.手動(dòng)拼接 SQL:挑戰(zhàn)傳統(tǒng)的方式
<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>發(fā)現(xiàn)花費(fèi)10958毫秒,比一條條插入數(shù)據(jù)性能提高十幾倍,但是和saveBatch性能相差不大
既然都驗(yàn)證都這了,我就在想,要不要使用JDBC批量插入進(jìn)行驗(yàn)證一下,看會(huì)不會(huì)出現(xiàn)原始的才是最好的結(jié)果
4.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,"計(jì)算機(jī)學(xué)院");
statement.setString(6,"廣東省廣州市番禺"+i+"號(hào)");
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 的性能會(huì)好點(diǎn),耗費(fèi)6667毫秒

但是感覺(jué)到這里以后,覺(jué)得時(shí)候還是比較長(zhǎng),有沒(méi)有可以再進(jìn)行優(yōu)化的方式,然后我就在ClientPreparedStatement類(lèi)中發(fā)現(xiàn)有一個(gè)叫做rewriteBatchedStatements 的屬性,從名字來(lái)看是要重寫(xiě)批操作的 Statement,前面batchHasPlainStatements 已經(jīng)是 false,取反肯定是 true,所以只要這參數(shù)是 true 就會(huì)進(jìn)行一波操作。rewriteBatchedStatements默認(rèn)是 false。
圖片

圖片
大家也可以自行網(wǎng)上搜索一下這個(gè)神奇的屬性然后我在url添加上這個(gè)屬性
圖片
然后繼續(xù)跑了下 mybatis-plus 自帶的 saveBatch,果然性能大大提高直接由原來(lái)的9204毫秒,提升到現(xiàn)在的3903毫秒
圖片
再來(lái)跑一下JDBC 的 executeBatch ,果然也提高了。
直接由原來(lái)的6667毫秒,提升到了3794毫秒

結(jié)果對(duì)比
批量保存方式 | 數(shù)據(jù)量(條) | 耗時(shí)(ms) |
單條循環(huán)插入 | 100000 | 195569 |
mybatis-plus saveBatch | 100000 | 9204 |
mybatis-plus saveBatch(添加 rewrite 參數(shù)) | 100000 | 3903 |
手動(dòng)拼接 SQL | 100000 | 6667 |
JDBC executeBatch | 100000 | 10958 |
JDBC executeBatch(添加 rewrite 參數(shù)) | 100000 | 3794 |
結(jié)論
通過(guò)實(shí)驗(yàn)結(jié)果,我們可以得出以下結(jié)論:
- mybatis-plus 的 saveBatch 方法相比單條循環(huán)插入在性能上有所提升,但仍然不夠理想。
- JDBC 的 executeBatch 方法在默認(rèn)情況下性能與 mybatis-plus 的 saveBatch 類(lèi)似,但通過(guò)設(shè)置 rewriteBatchedStatements 參數(shù)為 true 可顯著提高性能。
- rewriteBatchedStatements 參數(shù)的作用是將一批插入拼接成 insert into xxx values (a),(b),(c)... 這樣的一條語(yǔ)句形式,提高了性能。
優(yōu)化建議
如果您在項(xiàng)目中需要進(jìn)行批量插入操作,我建議考慮以下優(yōu)化方案:
- 如果使用 mybatis-plus,可以嘗試將 JDBC 連接字符串中的 rewriteBatchedStatements 參數(shù)設(shè)置為 true,以提高 saveBatch 方法的性能。

































