问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

如何优化MyBatis-Plus的批量插入性能

创作时间:
作者:
@小白创作中心

如何优化MyBatis-Plus的批量插入性能

引用
CSDN
1.
https://blog.csdn.net/u014109358/article/details/141430110

在数据库批量插入数据时,选择合适的方法可以显著提升性能。本文通过实际测试对比了多种常见的批量插入方式,并深入分析了MyBatis-Plus的实现原理,发现通过设置rewriteBatchedStatements参数可以大幅提升性能。

最近在压测一批接口时,发现接口处理速度慢得超出预期。经过定位,问题出在数据库批量保存这块。项目使用的是MyBatis-Plus,批量保存直接用的是MyBatis-Plus提供的saveBatch方法。

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

继续追踪代码,发现确实是for循环一条一条执行sqlSession.insert:

从这点来看,这个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究竟干了什么:

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

那为什么默认不给这个参数设置为true呢?原来是这样的:

  1. 如果批量语句中的某些语句失败,则默认重写会导致所有语句都失败。
  2. 批量语句的某些语句参数不一样,则默认重写会使得查询缓存未命中。

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

最后

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

批量保存方式
数据量(条)
耗时(ms)
单条循环插入
1000
121011
MyBatis-Plus saveBatch
1000
59927
MyBatis-Plus saveBatch(添加rewtire参数)
1000
2589
手动拼接sql
1000
2275
JDBC executeBatch
1000
55663
JDBC executeBatch(添加rewtire参数)
1000
324

所以如果有使用JDBC的Batch性能方面的需求,要将rewriteBatchedStatements设置为true,这样能提高很多性能。然后如果喜欢手动拼接SQL要注意一次拼接的数量,分批处理。

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

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号