调优 mybatis saveBatch 25倍性能

调优 mybatis saveBatch 25倍性能

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

这个项目用的是 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:

  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 要注意一次拼接的数量,分批处理。

转载自https://mp.weixin.qq.com/s/JIXh_maKHYgcOA19BGgczw

相关推荐
卷福同学6 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bingbingyihao2 小时前
多数据源 Demo
java·springboot
在努力的前端小白7 小时前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
一叶飘零_sweeeet9 小时前
从繁琐到优雅:Java Lambda 表达式全解析与实战指南
java·lambda·java8
艾伦~耶格尔10 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试
一只叫煤球的猫10 小时前
🕰 一个案例带你彻底搞懂延迟双删
java·后端·面试
最初的↘那颗心10 小时前
Flink Stream API 源码走读 - print()
java·大数据·hadoop·flink·实时计算
JH307311 小时前
Maven的三种项目打包方式——pom,jar,war的区别
java·maven·jar
带刺的坐椅12 小时前
轻量级流程编排框架,Solon Flow v3.5.0 发布
java·solon·workflow·flow·solon-flow
David爱编程12 小时前
线程调度策略详解:时间片轮转 vs 优先级机制,面试常考!
java·后端