如何优化 mybatis-plus 的批量插入

最近在压测一批接口,发现接口处理速度慢的有点超出预期,感觉很奇怪,后面定位发现是数据库批量保存这块很慢。

这个项目用的是 mybatis-plus,批量保存直接用的是 mybatis-plus 提供的 saveBatch。

我点进去看了下源码,感觉有点不太对劲:

图片

我继续追踪了下,从这个代码来看,确实是 for 循环一条一条执行了 sqlSession.insert,下面的 consumer 执行的就是上面的 sqlSession.insert:

图片

然后累计一定数量后,一批 flush。

从这点来看,这个 saveBach 的性能肯定比直接一条一条 insert 快。

我直接进行一个粗略的实验,简单创建了一张表来对比一波!

粗略的实验

1000条数据,一条一条插入

@Test
void MybatisPlusSaveOne() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mybatis plus save one");
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            //一条一条插入
            openTestService.save(openTest);
        }
        sqlSession.commit();
        stopWatch.stop();
        log.info("mybatis plus save one:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

图片

可以看到,执行一批 1000 条数的批量保存,耗费的时间是 121011 毫秒。

1000条数据用 mybatis-plus 自带的 saveBatch 插入

@Test
void MybatisPlusSaveBatch() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        List<OpenTest> openTestList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            openTestList.add(openTest);
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mybatis plus save batch");
        //批量插入
        openTestService.saveBatch(openTestList);
        sqlSession.commit();
        stopWatch.stop();
        log.info("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

图片

耗费的时间是 59927 毫秒,比一条一条插入快了一倍,从这点来看,效率还是可以的。

然后常见的还有一种利用拼接 sql 方式来实现批量插入,我们也来对比试试看性能如何。

1000条数据用手动拼接 sql 方式插入

搞个手动拼接:

图片

来跑跑下性能如何:

@Test
void MapperSaveBatch() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        List<OpenTest> openTestList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            openTestList.add(openTest);
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mapper save batch");
        //手动拼接批量插入
        openTestMapper.saveBatch(openTestList);
        sqlSession.commit();
        stopWatch.stop();
        log.info("mapper save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

图片

耗时只有 2275 毫秒,性能比 mybatis-plus 自带的 saveBatch 好了 26 倍!

这时,我又突然回想起以前直接用 JDBC 批量保存的接口,那都到这份上了,顺带也跑跑看!

1000条数据用 JDBC executeBatch 插入

@Test
void JDBCSaveBatch() throws SQLException {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    Connection connection = sqlSession.getConnection();
    connection.setAutoCommit(false);

    String sql = "insert into open_test(a,b,c,d,e,f,g,h,i,j,k) values(?,?,?,?,?,?,?,?,?,?,?)";
    PreparedStatement statement = connection.prepareStatement(sql);
    try {
        for (int i = 0; i < 1000; i++) {
            statement.setString(1,"a" + i);
            statement.setString(2,"b" + i);
            statement.setString(3, "c" + i);
            statement.setString(4,"d" + i);
            statement.setString(5,"e" + i);
            statement.setString(6,"f" + i);
            statement.setString(7,"g" + i);
            statement.setString(8,"h" + i);
            statement.setString(9,"i" + i);
            statement.setString(10,"j" + i);
            statement.setString(11,"k" + i);
            statement.addBatch();
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("JDBC save batch");
        statement.executeBatch();
        connection.commit();
        stopWatch.stop();
        log.info("JDBC save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        statement.close();
        sqlSession.close();
    }
}

图片

耗时是 55663 毫秒,所以 JDBC executeBatch 的性能跟 mybatis-plus 的 saveBatch 一样(底层一样)。

综上所述,拼接 sql 的方式实现批量保存效率最佳。

但是我又不太甘心,总感觉应该有什么别的法子,然后我就继续跟着 mybatis-plus 的源码 debug 了一下,跟到了 mysql 的驱动,突然发现有个 if 里面的条件有点显眼:

图片

就是这个叫 rewriteBatchedStatements 的玩意,从名字来看是要重写批操作的 Statement,前面batchHasPlainStatements 已经是 false,取反肯定是 true,所以只要这参数是 true 就会进行一波操作。

我看了下默认是 false。

图片

同时我也上网查了下 rewriteBatchedStatements 参数,好家伙,好像有用!我直接将 jdbcurl 加上了这个参数:

图片

然后继续跑了下 mybatis-plus 自带的 saveBatch,果然性能大大提高,跟拼接 SQL 差不多!

图片

顺带我也跑了下 JDBC 的 executeBatch ,果然也提高了。

图片

然后我继续 debug ,来探探 rewriteBatchedStatements 究竟是怎么 rewrite 的!

如果这个参数是 true,则会执行下面的方法且直接返回:

图片

看下 executeBatchedInserts 究竟干了什么:

图片

看到上面我圈出来的代码没,好像已经有点感觉了,继续往下 debug。

果然!sql 语句被 rewrite了:

图片

对插入而言,所谓的 rewrite 其实就是将一批插入拼接成 insert into xxx values (a),(b),(c)...这样一条语句的形式然后执行,这样一来跟拼接 sql 的效果是一样的。

那为什么默认不给这个参数设置为 true 呢?

原来是这样的:

  1. 如果批量语句中的某些语句失败,则默认重写会导致所有语句都失败。

  2. 批量语句的某些语句参数不一样,则默认重写会使得查询缓存未命中。

看起来影响不大,所以我给我的项目设置上了这个参数!

最后

稍微总结下我粗略的对比(虽然粗略,但实验结果符合原理层面的理解),如果你想更准确地实验,可以使用JMH,并且测试更多组数(如 5000,10000等)的情况。

批量保存方式数据量(条)耗时(ms)
单条循环插入1000121011
mybatis-plus saveBatch100059927
mybatis-plus saveBatch(添加rewtire参数)10002589
手动拼接sql10002275
jdbc executeBatch100055663
jdbc executeBatch(添加rewtire参数)1000324

所以如果有使用 jdbc 的 Batch 性能方面的需求,要将 rewriteBatchedStatements 设置为 true,这样能提高很多性能。

然后如果喜欢手动拼接 sql 要注意一次拼接的数量,分批处理。

参考链接:从120s到2.5s!看看人家的MyBatis批量插入数据优化,那叫一个优雅!

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: mybatis-plus的savebatch用法是批量插入数据的方法,可以将多条数据一次性插入到数据库中。使用该方法需要先创建一个实体类,然后通过该实体类的List集合来传递多条数据,最后调用savebatch方法即可完成批量插入操作。在使用该方法时需要注意,插入的数据必须符合数据库表的字段类型和约束条件,否则会插入失败。同时,该方法也支持自动生成主键的功能,可以通过在实体类的主键字段上添加注解来实现。 ### 回答2: Mybatis-plus是一个基于Mybatis的增强工具库,能够简化开发中的Mybatis操作。其中有一个saveBatch方法就是用来批量添加数据的,本文将详细介绍saveBatch的用法。 saveBatch方法的定义如下: ```java boolean saveBatch(Collection<T> entityList, int batchSize) ``` 其中,entityList表示待添加的数据列表,batchSize表示每批次添加的数量。 使用saveBatch方法的步骤如下: 1.在mapper接口中定义相应的方法。mapper接口需要继承BaseMapper接口,并指定类型参数,如下: ```java public interface UserMapper extends BaseMapper<User> { } ``` 2.实现mapper接口中的定义方法。如下: ```java public class UserMapperImpl extends ServiceImpl<UserMapper, User> implements UserMapper { } ``` 3.使用saveBatch方法添加数据。如下: ```java List<User> userList = new ArrayList<>(); for (int i = 0; i < 10000; i++) { User user = new User(); user.setName("Tom" + i); user.setAge(i); userList.add(user); } userMapper.saveBatch(userList, 1000); ``` 注:上述代码中的userMapper为UserMapper类型的对象。 以上是saveBatch方法的基本使用方法。需要注意的是,当批次量较大时,可以适当调大batchSize的值。同时,还需注意存在主键冲突的情况,需要在代码中进行处理。如果需要批量添加不同类型的数据,则需要在mapper接口中定义多个方法,并实现相应的实现类。 总之,Mybatis-plus的saveBatch方法可以极大地简化添加数据的操作,提高代码效率,也是业务开发中经常使用的一个方法。 ### 回答3: Mybatis-Plus是一个基于Mybatis的增强工具,可以轻松地操作数据库。其中,SaveBatch是Mybatis-Plus针对批量插入数据提供的一种方法。SaveBatch的使用方法如下: 1.定义实体类 首先,我们需要定义一个实体类,该实体类的属性与数据库表字段相对应。 例如,定义一个User实体类,在User实体类中定义需要保存的字段属性: ```java @Data public class User { private Long id; private String name; private Integer age; } ``` 2.定义Mapper接口 接着,我们需要定义一个Mapper接口,用于定义需要对数据库进行的操作,包括插入、查询、删除等操作。在Mapper接口中定义SaveBatch方法,用于批量插入数据。 例如,定义一个UserMapper接口,在UserMapper接口中定义SaveBatch方法: ```java public interface UserMapper extends BaseMapper<User> { void saveBatch(List<User> userList); } ``` 3.调用SaveBatch方法 最后,我们可以通过Mybatis-Plus提供的BaseMapper接口,调用SaveBatch方法,将需要插入的数据集合传递给SaveBatch方法即可。 例如,在业务代码中,需要批量插入userList数据集合,可以调用UserMapper接口的SaveBatch方法: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public void insertBatch(List<User> userList) { userMapper.saveBatch(userList); } } ``` 需要注意的是,在批量插入数据时,数据库表中的字段需要与实体类中的属性名称一一对应,否则会报错。另外,SaveBatch方法执行效率较高,相较于逐条插入数据,可大大提高数据插入的效率。 以上就是Mybatis-Plus SaveBatch的用法,希望对大家有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值