MyBatis-Plus的saveBatch批量插入为何效率很低,耗时很长,怎么解决?

Java技术 潘老师 3年前 (2021-07-14) 8509 ℃ (0) 扫码查看

由于项目是使用MyBatis-Plus开发的,用起来也确实比较方便,尤其是service层封装好的一些通用的增删改查方法,省去了不少sql语句的书写,但是在开发过程中,我也发现MyBatis-Plus的saveBatch批量插入方法针对MySQLOracle数据库可能会出现效率贼低的情况,下面我们先具体来说说产生的原因是什么。

一、MySQL数据库

针对MySQL数据库saveBatch批量插入效率比较低,是比较好解决的,一般都是由于数据库连接url上没有配置批量操作的属性,只需要在url上加上如下属性即可:

rewriteBatchedStatements=true

即类似如下:

jdbc:mysql://数据库地址/数据库名?useUnicode=true&characterEncoding=UTF8&allowMultiQueries=true&rewriteBatchedStatements=true

加上之后,你就会发现,saveBatch的速度直线提升,效果还是很不错的,一万条数据估计也就在几百毫秒。

二、Oracle数据库

Oracle数据库的问题就比较大了,而且至今潘老师也没找到一个比较完美的解决方案,此次写这篇博客也正是由于Oracle数据库saveBatch效率贼低引起的,先看下图,批量插入一万条数据(MyBatis-Plus的saveBatch默认一次1000条,1w条会分10次,当然你也可以设置Batch Size),耗时竟然达到10s多,简直不能忍啊,堪比龟速!
MyBatis-Plus的saveBatch批量插入为何效率很低,耗时很长,怎么解决?
于是就开始各种排查,经过一番仔细debug、翻源码,反复测试对比验证,最终发现是因为MyBatis-Plus的针对Oracle主键序列生成策略导致的,在上图打印的日志可以看出,在每次打印参数之前,都会先执行下SELECT XXX.NEXTVAL FROM DUAL,这是我们在实体Entity上加上了MyBatis-Plus的注解@KeySequence(value = "XXX")引起的,本来预想的1万条只需要和数据库交互10次就解决了,现在看打印日志情况,预估是insert和查询序列合计预计2万次交互,于是,测试了下去掉@KeySequence注解,手工给id赋好值,再次批量保存1万条,结果如下:
MyBatis-Plus的saveBatch批量插入为何效率很低,耗时很长,怎么解决?
好家伙,直接干到1s多点,整整节约了10倍的时间,病根终于确定了,就是@KeySequence注解导致的

但你这就想把锅甩给MyBatis-Plus那就大错特错了,这锅归根到底其实还是MyBatis的,为什么呢?经过潘老师一番深入探查,发现正如MyBatis-Plus官方所说,只对MyBatis做扩展却不改变,做一对快乐的好基友,@KeySequence(value = "XXX")作用就是结合KeyGenerator来实现自动化生成主键并回填,但是MyBatis-Plus的所有KeyGenerator也是从MyBatis继承扩展而来,而使用了之后,对于Oracle而言就相当于在insert语句前加上了selectKey语句,类似如下:

<selectKey keyColumn="id" keyProperty="id" resultType="int" order="BEFORE">
       select XXX.nextval from dual
</selectKey>

这是不是很熟悉,就是我们学MyBatis时候学的啊,MyBatis-Plus只不过是把它变成了@KeySequence注解,省去了你写这段xml了而已,而所有问题的源头就来自于这段xml,潘老师亲自测试,在insert前加上这段xml后使用Mybatis原生的SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH)批量插入,发现批量插入无效,耗时和MyBatis-plus差不多,1w条大概10多秒,但是一旦去掉这段xml,再使用原生的Batch插入,我的天,竟然只要500多毫秒,也就是0.5秒,这差距在20倍左右,一直以为是MyBatis-Plus的锅,原来是MyBatis的锅,这下舒服了,可以安心地用MyBatis-Plus了,除非你不用mybatis了~
MyBatis-Plus的saveBatch批量插入为何效率很低,耗时很长,怎么解决?

解决方案

后期补充:最优解:

今天经过潘老师继续翻看源码,还真让潘老师发现了一个新的非常好的解决方案,原来我们的xml中sql都是由MyBatis-Plus动态帮我们生成的,包括id为insert的,经过测试发现,如果我们在xml中自己再新定义一个id为insert的sql语句(注意id必须为insert),就将原来的默认生成的覆盖掉了,直接使用我们自己定义的,那么问题就迎刃而解了,具体如下:
a)去掉KeyGenerator实体对象的配置
b)去掉@KeySequence注解
c)id-type: 设置为AUTO
d)在实体对应的xml新增id为insert的sql,类似如下:

<insert id="insert" parameterType="user">
        insert into t_user(id, username, password)
        values(SEQ_USER.NEXTVAL,#{username},#{password})
</insert>

e)继续调用MyBatis-Plus的saveBatch或save,都会走我们写的这个insert对应的xml
f)测试后1w条大概在几百毫秒。
完美解决,撒花庆祝~

原可选方案:

1)其中一种比较理想的解决方案是使用触发器来解决,抛弃@KeySequence注解,在插入数据Entity时,由触发器生成序列给id赋值,缺点是无法将id返回到实体,况且我们项目组要求不允许使用触发器,因此就放弃了这种方案。

2)网上还有一种方案就是使用begin end包裹所有sql,不过经过亲测,发现比使用MyBatis-Plus主键生成策略还坑,时间直接再翻10倍,直接忽略吧,因为它底层根本不是Batch,而且我还试了一次10w条,直接就挂了(抛异常,提示变量太多),1W条耗时120多秒,比龟速还龟速!
以下是网上方案(大批量数据强烈不建议使用):
存储过程不是写死在数据库中,而是在Mapper.xml中,使用begin end包裹foreach批量生成insert语句作为一个整体提交给Oracle,id由序列在数据库直接生成,类似如下(注意两个地方的分号不能少):

<insert id="insertBatch" parameterType="java.util.List">
        begin
        <foreach collection="list" item="user">
            insert into t_user(id, username, password)
            values(SEQ_USER.NEXTVAL,#{user.username},#{user.password});
        </foreach>
        end;
    </insert>
注意:这种方案在写插入SQL如果想通用性比较好的话,需要针对部分有默认值的字段特别处理,因为你提交的是Entity集合,如果这些字段存在为null的也会作为null插入,而不会使用默认值,这是因为insert语句是全字段声明式插入,所以即使某个字段为空,它也不会使用数据库中的默认值,因此解决方案一种是有默认值字段的不在insert语句中声明,不过这样通用性不好,另外一种就是对其进行动态判断

3)第三种方案就是不使用数据库序列生成主键,我们自己使用java代码写个主键生成策略(类似UUID这种),这样就避免了和数据库的频繁交互,我觉得这种方案也比较靠谱。

4)还有一种和第2种类似,不过是使用UNION ALL来联合插入的,避免了begin end,但是据说只能UNION ALL2000条,多了就会出问题。

5)最后一种,回归本源,使用原生的MyBatis的SqlSession中使用Batch模式吧,别使用selectKey就行

总结

其实,当你选择了一种框架,在享受它带来的便利时,也要能承受它给你带来的不利,我们需要做的只是在利弊中取舍,尽可能扬长避短,趋利避害,除非你自己开发个更优秀的框架,那也是贼好的!

如果你还有什么更好的解决方案,请在评论区留言,告诉下潘老师啊~


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/3343.html
喜欢 (15)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】