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

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

一、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多,简直不能忍啊,堪比龟速!

在这里插入图片描述
于是就开始各种排查,经过一番仔细debug、翻源码,反复测试对比验证,最终发现是因为MyBatis-Plus的针对Oracle主键序列生成策略导致的,在上图打印的日志可以看出,在每次打印参数之前,都会先执行下SELECT XXX.NEXTVAL FROM DUAL,这是我们在实体Entity上加上了MyBatis-Plus的注解@KeySequence(value = “XXX”)引起的,本来预想的1万条只需要和数据库交互10次就解决了,现在看打印日志情况,预估是insert和查询序列合计预计2万次交互,于是,测试了下去掉@KeySequence注解,手工给id赋好值,再次批量保存1万条,结果如下:
在这里插入图片描述
好家伙,直接干到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了~

在这里插入图片描述
解决方案
后期补充:最优解:

今天经过潘老师继续翻看源码,还真让潘老师发现了一个新的非常好的解决方案,原来我们的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就行

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

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

  • 13
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: mybatis-plus的savebatch用法是批量插入数据的方法,可以将多条数据一次性插入到数据库中。使用该方法需要先创建一个实体类,然后通过该实体类的List集合来传递多条数据,最后调用savebatch方法即可完成批量插入操作。在使用该方法时需要注意,插入的数据必须符合数据库表的字段类型和约束条件,否则会插入失败。同时,该方法也支持自动生成主键的功能,可以通过在实体类的主键字段上添加注解来实现。 ### 回答2: Mybatis-plus是一个基于Mybatis的增强工具库,能够简化开发中的Mybatis操作。其中有一个saveBatch方法就是用来批量添加数据的,本文将详细介绍saveBatch的用法。 saveBatch方法的定义如下: ```java boolean saveBatch(Collection<T> entityList, int batchSize) ``` 其中,entityList表示待添加的数据列表,batchSize表示每批次添加的数量。 使用saveBatch方法的步骤如下: 1.在mapper接口中定义相应的方法。mapper接口需要继承BaseMapper接口,并指定类型参数,如下: ```java public interface UserMapper extends BaseMapper<User> { } ``` 2.实现mapper接口中的定义方法。如下: ```java public class UserMapperImpl extends ServiceImpl<UserMapper, User> implements UserMapper { } ``` 3.使用saveBatch方法添加数据。如下: ```java List<User> userList = new ArrayList<>(); for (int i = 0; i < 10000; i++) { User user = new User(); user.setName("Tom" + i); user.setAge(i); userList.add(user); } userMapper.saveBatch(userList, 1000); ``` 注:上述代码中的userMapper为UserMapper类型的对象。 以上是saveBatch方法的基本使用方法。需要注意的是,当批次量较大时,可以适当调大batchSize的值。同时,还需注意存在主键冲突的情况,需要在代码中进行处理。如果需要批量添加不同类型的数据,则需要在mapper接口中定义多个方法,并实现相应的实现类。 总之,Mybatis-plus的saveBatch方法可以极大地简化添加数据的操作,提高代码效率,也是业务开发中经常使用的一个方法。 ### 回答3: Mybatis-Plus是一个基于Mybatis的增强工具,可以轻松地操作数据库。其中,SaveBatchMybatis-Plus针对批量插入数据提供的一种方法。SaveBatch的使用方法如下: 1.定义实体类 首先,我们需要定义一个实体类,该实体类的属性与数据库表字段相对应。 例如,定义一个User实体类,在User实体类中定义需要保存的字段属性: ```java @Data public class User { private Long id; private String name; private Integer age; } ``` 2.定义Mapper接口 接着,我们需要定义一个Mapper接口,用于定义需要对数据库进行的操作,包括插入、查询、删除等操作。在Mapper接口中定义SaveBatch方法,用于批量插入数据。 例如,定义一个UserMapper接口,在UserMapper接口中定义SaveBatch方法: ```java public interface UserMapper extends BaseMapper<User> { void saveBatch(List<User> userList); } ``` 3.调用SaveBatch方法 最后,我们可以通过Mybatis-Plus提供的BaseMapper接口,调用SaveBatch方法,将需要插入的数据集合传递给SaveBatch方法即可。 例如,在业务代码中,需要批量插入userList数据集合,可以调用UserMapper接口的SaveBatch方法: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public void insertBatch(List<User> userList) { userMapper.saveBatch(userList); } } ``` 需要注意的是,在批量插入数据时,数据库表中的字段需要与实体类中的属性名称一一对应,否则会报错。另外,SaveBatch方法执行效率较高,相较于逐条插入数据,可大大提高数据插入的效率。 以上就是Mybatis-Plus SaveBatch的用法,希望对大家有所帮助。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值