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

这个项目用的是 mybatis-plus,批量保存直接用的是 mybatis-plus 供给的 saveBatch。 我点进去看了下源码,感觉有点不太对劲:

【MyBatis】saveBatch 性能调优

持续追寻了下,从这个代码来看,确实是 for 循环一条一条履行了 sqlSession.insert,下面的 consumer 履行的便是上面的 sqlSession.insert:

【MyBatis】saveBatch 性能调优

然后累计必定数量后,一批 flush。从这点来看,这个 saveBach 的功能必定比直接一条一条 insert 快。

我直接进行一个大略的试验,简单创建了一张表来比照一波!

1、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 毫秒

2、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();
    }
}

【MyBatis】saveBatch 性能调优

消耗的时刻是 59927 毫秒,比一条一条刺进快了一倍,从这点来看,功率还是能够的。

然后常见的还有一种运用拼接 SQL 方法来完成批量刺进,咱们也来比照试试看功能怎么。

3、1000 条数据用手动拼接 SQL 方法刺进, 搞个手动拼接:

【MyBatis】saveBatch 性能调优

来跑跑下功能怎么:

@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 批量保存的接口,那都到这份上了,顺带也跑跑看!

4、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();
    }
}

【MyBatis】saveBatch 性能调优

耗时是 55663 毫秒,所以 JDBC executeBatch 的功能跟 mybatis-plus 的 saveBatch 相同(底层相同)。

综上所述,拼接 SQL 的方法完成批量保存功率最佳

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

【MyBatis】saveBatch 性能调优

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

我看了下默许是 false。

【MyBatis】saveBatch 性能调优

同时我也上网查了下 rewriteBatchedStatements 参数,好家伙,如同有用!

【MyBatis】saveBatch 性能调优

直接将 jdbcurl 加上了这个参数:

【MyBatis】saveBatch 性能调优

然后持续跑了下 mybatis-plus 自带的 saveBatch,公然功能大大进步,跟拼接 SQL 差不多!

【MyBatis】saveBatch 性能调优

顺带我也跑了下 JDBC 的 executeBatch ,公然也进步了。

【MyBatis】saveBatch 性能调优

然后我持续 debug ,来探探 rewriteBatchedStatements 终究是怎么 rewrite 的! 假如这个参数是 true,则会履行下面的方法且直接返回:

【MyBatis】saveBatch 性能调优

看下 executeBatchedInserts 终究干了什么:

【MyBatis】saveBatch 性能调优

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

公然!SQL 句子被 rewrite了:

【MyBatis】saveBatch 性能调优

对刺进而言,所谓的 rewrite 其实便是将一批刺进拼接成 insert into xxx values (a),(b),(c)…这样一条句子的方式然后履行,这样一来跟拼接 SQL 的效果是相同的。

那为什么默许不给这个参数设置为 true 呢?主要有以下两点:

假如批量句子中的某些句子失利,则默许重写会导致一切句子都失利。

批量句子的某些句子参数不相同,则默许重写会使得查询缓存未命中。

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

最终

略微总结下我大略的比照(尽管大略,但试验成果符合原理层面的了解),假如你想更准确地做试验,能够运用 JMH,而且测验更多组数(如 5000,10000等)的情况。

【MyBatis】saveBatch 性能调优

所以假如有运用 JDBC 的 Batch 功能方面的需求,要将 rewriteBatchedStatements 设置为 true,这样能进步许多功能。

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