关于python:调优-mybatis-saveBatch-25倍性能

32次阅读

共计 4361 个字符,预计需要花费 11 分钟才能阅读完成。

你好,我是 yes。
最近在压测一批接口,发现接口处理速度慢的有点超出预期,感觉很奇怪,前面定位发现是数据库批量保留这块很慢。
这个我的项目用的是 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 呢?
我简略问了下 ChatGPT:

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

正文完
 0