【MyBatis-plus】saveBatch 性能调优和【MyBatis】的数据批量入库 ...

铁佛  金牌会员 | 2024-6-22 23:29:33 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 542|帖子 542|积分 1626

总结最优的两种方法:

方法1:
使用了【MyBatis-plus】saveBatch 但是数据入库效率依旧很慢,那大概是是由于JDBC没有配置,saveBatch 批量写入并没有见效哦!!!
详细配置如下:批量数据入库:rewriteBatchedStatements=true
  1. # 数据源
  2.           master:
  3.             driver-class-name: org.postgresql.Driver
  4.             url: jdbc:postgresql://127.0.0.1:5444/mxpt_business_databases?useUnicode=true&characterEncoding=utf8&currentSchema=public&stringtype=unspecified&rewriteBatchedStatements=true
  5.             username: postgres
  6.             password: postgres
  7.             schema: public
复制代码
方法2:
使用【MyBatis】举行数据的批量入库:拼接sql语句,每1000条数据入库一次。
  1. @Override
  2.     public String insertBoundValueListToDatabase(List<ResourceCalcSceneBoundValue> list)
  3.     {
  4.         //1.先删除原有场次和工程的数据,再进行导入
  5.         ResourceCalcSceneBoundValue gongkuangValue = list.get(0);
  6.         Long scprodId = gongkuangValue.getScprodId();
  7.         Long gongkuangId = gongkuangValue.getBoundId();
  8.         List<ResourceCalcSceneBoundValue> listValue = resourceCalcSceneBoundValueMapper.selectResourceCalcSceneBoundValueList(gongkuangValue);
  9.         if(listValue != null && listValue.size() > 0){
  10.             resourceCalcSceneBoundValueMapper.deleteBoundValueByScprodIdAndBoundId(scprodId, gongkuangId);
  11.         }
  12.         //2.将结果插入到数据库中
  13.         if (list.size() > 0) {
  14.             //条数为1
  15.             if(list.size() == 1){
  16.                 resourceCalcSceneBoundValueMapper.insertResourceCalcSceneBoundValueList(list.subList(0, 1));
  17.             }
  18.             //由于数据库对于插入字段的限制,在这里对批量插入的数据进行分批处理
  19.             int batchCount = 120;//每批commit的个数
  20.             int batchLastIndex = batchCount - 1;// 每批最后一个的下标
  21.             for (int index = 0; index < list.size() - 1; ) {
  22.                 if (batchLastIndex > list.size() - 1) {
  23.                     batchLastIndex = list.size() - 1;
  24.                     resourceCalcSceneBoundValueMapper.insertResourceCalcSceneBoundValueList(list.subList(index, batchLastIndex + 1));
  25.                     break;// 数据插入完毕,退出循环
  26.                 } else {
  27.                     resourceCalcSceneBoundValueMapper.insertResourceCalcSceneBoundValueList(list.subList(index, batchLastIndex + 1));
  28.                     index = batchLastIndex + 1;// 设置下一批下标
  29.                     batchLastIndex = index + (batchCount - 1);
  30.                 }
  31.             }
  32.             return "边界过程数据入库成功! 条数为:"+list.size()+"条。 ";
  33.         }
  34.         return "数据条数为0。";
  35.     }
复制代码
xml代码:
  1. <insert id="insertResourceCalcSceneBoundValueList" parameterType="java.util.List" useGeneratedKeys="false">
  2.         INSERT INTO resource_calc_scene_bound_value
  3.         (scprod_id, bound_id, tm, flow, water, kurong, inq, stcd, remark, jp, kaidu, kgnum)
  4.         VALUES
  5.         <foreach collection="list" item="item" index="index" separator=",">
  6.             (#{item.scprodId,jdbcType=INTEGER}
  7.             ,#{item.boundId,jdbcType=INTEGER}
  8.             ,#{item.tm,jdbcType=TIMESTAMP}
  9.             ,#{item.flow,jdbcType=NUMERIC}
  10.             ,#{item.water,jdbcType=NUMERIC}
  11.             ,#{item.kurong,jdbcType=NUMERIC}
  12.             ,#{item.inq,jdbcType=NUMERIC}
  13.             ,#{item.stcd,jdbcType=VARCHAR}
  14.             ,#{item.remark,jdbcType=VARCHAR}
  15.             ,#{item.jp,jdbcType=NUMERIC}
  16.             ,#{item.kaidu,jdbcType=NUMERIC}
  17.             ,#{item.kgnum,jdbcType=INTEGER})
  18.         </foreach>
  19.     </insert>
复制代码
参考博客:

https://www.cnblogs.com/natee/p/17428877.html
大神总结的超级详细!!!
一起学习!!!
发现接口处理速率慢的有点超出预期,感觉很希奇,背面定位发现是数据库批量生存这块很慢。
这个项目用的是 mybatis-plus,批量生存直接用的是 mybatis-plus 提供的 saveBatch。 我点进去看了下源码,感觉有点不太对劲:

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

然后累计肯定命量后,一批 flush。从这点来看,这个 saveBach 的性能肯定比直接一条一条 insert 快。
1、1000条数据,一条一条插入
  1. @Test
  2. void MybatisPlusSaveOne() {
  3.     SqlSession sqlSession = sqlSessionFactory.openSession();
  4.     try {
  5.         StopWatch stopWatch = new StopWatch();
  6.         stopWatch.start("mybatis plus save one");
  7.         for (int i = 0; i < 1000; i++) {
  8.             OpenTest openTest = new OpenTest();
  9.             openTest.setA("a" + i);
  10.             openTest.setB("b" + i);
  11.             openTest.setC("c" + i);
  12.             openTest.setD("d" + i);
  13.             openTest.setE("e" + i);
  14.             openTest.setF("f" + i);
  15.             openTest.setG("g" + i);
  16.             openTest.setH("h" + i);
  17.             openTest.setI("i" + i);
  18.             openTest.setJ("j" + i);
  19.             openTest.setK("k" + i);
  20.             //一条一条插入
  21.             openTestService.save(openTest);
  22.         }
  23.         sqlSession.commit();
  24.         stopWatch.stop();
  25.         log.info("mybatis plus save one:" + stopWatch.getTotalTimeMillis());
  26.     } finally {
  27.         sqlSession.close();
  28.     }
  29. }
复制代码

可以看到,执行一批 1000 条数的批量生存,淹灭的时间是 121011 毫秒。
2、1000条数据用 mybatis-plus 自带的 saveBatch 插入
  1. @Test
  2. void MybatisPlusSaveBatch() {
  3.     SqlSession sqlSession = sqlSessionFactory.openSession();
  4.     try {
  5.         List<OpenTest> openTestList = new ArrayList<>();
  6.         for (int i = 0; i < 1000; i++) {
  7.             OpenTest openTest = new OpenTest();
  8.             openTest.setA("a" + i);
  9.             openTest.setB("b" + i);
  10.             openTest.setC("c" + i);
  11.             openTest.setD("d" + i);
  12.             openTest.setE("e" + i);
  13.             openTest.setF("f" + i);
  14.             openTest.setG("g" + i);
  15.             openTest.setH("h" + i);
  16.             openTest.setI("i" + i);
  17.             openTest.setJ("j" + i);
  18.             openTest.setK("k" + i);
  19.             openTestList.add(openTest);
  20.         }
  21.         StopWatch stopWatch = new StopWatch();
  22.         stopWatch.start("mybatis plus save batch");
  23.         //批量插入
  24.         openTestService.saveBatch(openTestList);
  25.         sqlSession.commit();
  26.         stopWatch.stop();
  27.         log.info("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());
  28.     } finally {
  29.         sqlSession.close();
  30.     }
  31. }
复制代码

淹灭的时间是 59927 毫秒,比一条一条插入快了一倍,从这点来看,效率还是可以的。
然后常见的另有一种使用拼接 SQL 方式来实现批量插入,我们也来对比试试看性能怎样。
3、1000 条数据用手动拼接 SQL 方式插入, 搞个手动拼接:

来跑跑下性能怎样:
  1. @Test
  2. void MapperSaveBatch() {
  3.     SqlSession sqlSession = sqlSessionFactory.openSession();
  4.     try {
  5.         List<OpenTest> openTestList = new ArrayList<>();
  6.         for (int i = 0; i < 1000; i++) {
  7.             OpenTest openTest = new OpenTest();
  8.             openTest.setA("a" + i);
  9.             openTest.setB("b" + i);
  10.             openTest.setC("c" + i);
  11.             openTest.setD("d" + i);
  12.             openTest.setE("e" + i);
  13.             openTest.setF("f" + i);
  14.             openTest.setG("g" + i);
  15.             openTest.setH("h" + i);
  16.             openTest.setI("i" + i);
  17.             openTest.setJ("j" + i);
  18.             openTest.setK("k" + i);
  19.             openTestList.add(openTest);
  20.         }
  21.         StopWatch stopWatch = new StopWatch();
  22.         stopWatch.start("mapper save batch");
  23.         //手动拼接批量插入
  24.         openTestMapper.saveBatch(openTestList);
  25.         sqlSession.commit();
  26.         stopWatch.stop();
  27.         log.info("mapper save batch:" + stopWatch.getTotalTimeMillis());
  28.     } finally {
  29.         sqlSession.close();
  30.     }
  31. }
复制代码

耗时只有 2275 毫秒,性能比 mybatis-plus 自带的 saveBatch 好了 26 倍!
这时,我又突然回想起以前直接用 JDBC 批量生存的接口,那都到这份上了,顺带也跑跑看!
4、1000 条数据用 JDBC executeBatch 插入
  1. @Test
  2. void JDBCSaveBatch() throws SQLException {
  3.     SqlSession sqlSession = sqlSessionFactory.openSession();
  4.     Connection connection = sqlSession.getConnection();
  5.     connection.setAutoCommit(false);
  6.     String sql = "insert into open_test(a,b,c,d,e,f,g,h,i,j,k) values(?,?,?,?,?,?,?,?,?,?,?)";
  7.     PreparedStatement statement = connection.prepareStatement(sql);
  8.     try {
  9.         for (int i = 0; i < 1000; i++) {
  10.             statement.setString(1,"a" + i);
  11.             statement.setString(2,"b" + i);
  12.             statement.setString(3, "c" + i);
  13.             statement.setString(4,"d" + i);
  14.             statement.setString(5,"e" + i);
  15.             statement.setString(6,"f" + i);
  16.             statement.setString(7,"g" + i);
  17.             statement.setString(8,"h" + i);
  18.             statement.setString(9,"i" + i);
  19.             statement.setString(10,"j" + i);
  20.             statement.setString(11,"k" + i);
  21.             statement.addBatch();
  22.         }
  23.         StopWatch stopWatch = new StopWatch();
  24.         stopWatch.start("JDBC save batch");
  25.         statement.executeBatch();
  26.         connection.commit();
  27.         stopWatch.stop();
  28.         log.info("JDBC save batch:" + stopWatch.getTotalTimeMillis());
  29.     } finally {
  30.         statement.close();
  31.         sqlSession.close();
  32.     }
  33. }
复制代码

耗时是 55663 毫秒,所以 JDBC executeBatch 的性能跟 mybatis-plus 的 saveBatch 一样(底层一样)。
综上所述,拼接 SQL 的方式实现批量生存效率最佳。
但是我又不太甘心,总感觉应该有什么别的法子,然后我就继续跟着 mybatis-plus 的源码 debug 了一下,跟到了 MySQL 的驱动,突然发现有个 if 内里的条件有点显眼:

就是这个叫 rewriteBatchedStatements 的玩意,从名字来看是要重写批利用的 Statement,前面batchHasPlainStatements 已经是 false,取反肯定是 true,所以只要这参数是 true 就会举行一波利用。
我看了下默认是 false。

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

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

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

看下 executeBatchedInserts 究竟干了什么:

看到上面我圈出来的代码没,好像已经有点感觉了,继续往下 debug。
果然!SQL 语句被 rewrite了:

对插入而言,所谓的 rewrite 实在就是将一批插入拼接成 insert into xxx values (a),(b),©…这样一条语句的形式然后执行,这样一来跟拼接 SQL 的结果是一样的。
那为什么默认不给这个参数设置为 true 呢?主要有以下两点:
如果批量语句中的某些语句失败,则默认重写会导致全部语句都失败。
批量语句的某些语句参数不一样,则默认重写会使得查询缓存未命中。
看起来影响不大,所以我给我的项目设置上了这个参数!
末了
稍微总结下我大略的对比(虽然大略,但实行结果符合原理层面的理解),如果你想更正确地做实行,可以使用 JMH,而且测试更多组数(如 5000,10000等)的环境。

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

铁佛

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表