MyBatis-Plus 的 saveBatch
方法是 ORM 框架中批量插入的核心功能,理解其实现原理和优化技巧对开发高性能应用至关重要。
在我们的userServiceTest类中定义一个插入数据的方法:
private User buildUser(int i) {
User user = new User();
user.setUsername("user"+i);
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
return user;
}
现在,想要往数据库里面插入10万条数据,现在有两种实现方法:
第一种:使用for循环插入:
//普通的增加十万条数据
@Test
public void TestOneByOne(){
//记录开始时间
long startTime = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
userService.save(buildUser(i));
//记录结束时间
long endTime = System.currentTimeMillis();
//总耗时
System.out.println("总耗时:" + (endTime - startTime) + "ms");
}
}
耗时:10min(因为电脑性能原因不方便演示,之前试过一次)
第二种:使用MybatisPlus里面Service提供的saveBatch()方法分批次插入
//批量插入,一次插入1000条数据,总共插入10万条数据
@Test
public void TestBatchInsert(){
List<User> userList = new ArrayList<>(1000);
//记录开始时间
long startTime = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
userList.add(buildUser(i));
if(i % 1000 == 0){
userService.saveBatch(userList);
//清空集合
userList.clear();
}
}
//记录结束时间
long endTime = System.currentTimeMillis();
//总耗时
System.out.println("总耗时:" + (endTime - startTime) + "ms");
}
结果:十万条数据成功插入,总耗时约43s
但是,我们不禁思考,Service提供的saveBatch()方法只有这点神通嘛?
答案是否定的,我们可以让他更快!
那么如何做呢?让我们进入到其中的源码部分
//ServiceImpl.java
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
继续深入
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.sqlSessionFactory, this.log, list, batchSize, consumer);
}
//SqlHelper.java
public static <E> boolean executeBatch(SqlSessionFactory sqlSessionFactory, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(sqlSessionFactory, log, sqlSession -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
i++;
}
});
}
我们会发现,他的底层居然是依靠for循环一个一个插入,并不是我们想象中的类似insert into [表名] valus …
此处,如果你需要这样做,你需要去开启一个参数:
rewriteBatchedStatements=true
无优化时:生成多条INSERT语句(INSERT INTO ... VALUES (...)
),等同于第一种普通for循环,一条一条插入的做法,性能低下
优化后:合并为单条批量语法(INSERT INTO ... VALUES (...),(...),...
)
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true//已开启
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
加入后继续执行
总耗时:约12s,性能大大提升!