• MyBatis(十):Mybatis 几种批量操作的对比


    本章主要讲解几种批量处理的用法及对别,批量处理一般用法包含以下几种:

    1)普通foreach处理(没循环一次执行一次与mysql服务器交互操作),实际上也是采用的ExecutorType.SIMPLE;

    2)使用ExecutorType.BATCH批量处理方法;

    3)拼接SQL,一次批量提交给Mysql多个插入语句到mysql服务器端,执行批量操作。

    下边针对这几种方案分别进行示例展示用法,以及优缺点对比。

    新建maven项目具体工作参考:《MyBatis(九):Mybatis Java API批量操作(增、删、改、查)
    在spring-config.xml中添加sqlSessionTemplate bean:

       <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
           <constructor-arg index="0" ref="sqlSessionFactory" />
           <!--<constructor-arg index="1" value="BATCH" />-->
       </bean>

    新建测试类:com.dx.test.TestBatchInsert.java

    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Random;
    
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import com.dx.test.mapper.ArticleCategoryMapper;
    import com.dx.test.model.ArticleCategory;
    import com.dx.test.model.enums.DataStatus;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({ "classpath:spring-config.xml" })
    public class TestBatchInsert {
       @Autowired
       private SqlSessionTemplate sqlSessionTemplate; // 引入注入的sqlSessionTemplate bean实例
       @Autowired
       private ArticleCategoryMapper articleCategoryMapper; // 参考上篇文章该类的定义。
    
    }

    mybatis 批量处理测试:

    方案1)采用mybatis foreach循环插入方案:

       @Test
       public void testWithSimple() {
          long start = System.currentTimeMillis();
          SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.SIMPLE, false);
          ArticleCategoryMapper articleCategoryMapper=sqlSession.getMapper(ArticleCategoryMapper.class);
          
          List<ArticleCategory> list = new ArrayList<>();
          for (int i = 0; i < 10000; i++) {
             ArticleCategory articleCategory = new ArticleCategory();
             articleCategory.setTitle("title_" + new Random().nextInt(1000) + "_" + new Random().nextInt(1000));
             articleCategory.setDescription("category description");
             articleCategory.setImgSrc("http://www.test.com/img/category/img-" + new Random().nextInt(1000) + ".gif");
             articleCategory.setState(DataStatus.Living);
             articleCategory.setCreateTime(new Date());
             articleCategory.setCreateUser("create user");
             articleCategory.setCreateUserId("user-" + new Random().nextInt(1000));
    
             articleCategoryMapper.insert(articleCategory);
          }
          sqlSession.commit();
          sqlSession.clearCache();
          sqlSession.close();
    
          long stop = System.currentTimeMillis();
          System.out.println("testWithExecutorType.SIMPLE:" + (stop - start) + "ms");
       }

    执行该测试方法:

    ==>  Preparing: INSERT INTO article_category (title, img_src, description, state, create_user, create_user_id, create_time, update_user, update_user_id, update_time, version) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, now(), 0) 
    ==> Parameters: title_159_754(String), http://www.test.com/img/category/img-606.gif(String), category description(String), 0(Integer), create user(String), user-740(String), null, null
    <==    Updates: 1
    。。。
    ==>  Preparing: INSERT INTO article_category (title, img_src, description, state, create_user, create_user_id, create_time, update_user, update_user_id, update_time, version) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, now(), 0) 
    ==> Parameters: title_416_561(String), http://www.test.com/img/category/img-72.gif(String), category description(String), 0(Integer), create user(String), user-98(String), null, null
    <==    Updates: 1
    ==>  Preparing: INSERT INTO article_category (title, img_src, description, state, create_user, create_user_id, create_time, update_user, update_user_id, update_time, version) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, now(), 0) 
    ==> Parameters: title_967_656(String), http://www.test.com/img/category/img-897.gif(String), category description(String), 0(Integer), create user(String), user-620(String), null, null
    <==    Updates: 1
    ==>  Preparing: INSERT INTO article_category (title, img_src, description, state, create_user, create_user_id, create_time, update_user, update_user_id, update_time, version) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, now(), 0) 
    ==> Parameters: title_399_676(String), http://www.test.com/img/category/img-819.gif(String), category description(String), 0(Integer), create user(String), user-889(String), null, null
    <==    Updates: 1
    testWithExecutorType.SIMPLE::8163ms

    测试结果发现耗时为:8163ms,而且从打印日志,可以看出该类执行是一条一条的插入的,而且每次插入前都要Preparing插入sql,这个是比较耗时的。

    方案2)采用mybatis batch方案(使用ExecutorType.BATCH):

       @Test
       public void testWithBatch() {
          long start = System.currentTimeMillis();
          SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
          ArticleCategoryMapper articleCategoryMapper=sqlSession.getMapper(ArticleCategoryMapper.class);
          List<ArticleCategory> list = new ArrayList<>();
          for (int i = 0; i < 10000; i++) {
             ArticleCategory articleCategory = new ArticleCategory();
             articleCategory.setTitle("title_" + new Random().nextInt(1000) + "_" + new Random().nextInt(1000));
             articleCategory.setDescription("category description");
             articleCategory.setImgSrc("http://www.test.com/img/category/img-" + new Random().nextInt(1000) + ".gif");
             articleCategory.setState(DataStatus.Living);
             articleCategory.setCreateTime(new Date());
             articleCategory.setCreateUser("create user");
             articleCategory.setCreateUserId("user-" + new Random().nextInt(1000));
    
             articleCategoryMapper.insert(articleCategory);
          }
          sqlSession.commit();
          sqlSession.clearCache();
          sqlSession.close();
    
          long stop = System.currentTimeMillis();
          System.out.println("testWithExecutorType.BATCH:" + (stop - start) + "ms");
       }

    执行该测试方法:

    ==>  Preparing: INSERT INTO article_category (title, img_src, description, state, create_user, create_user_id, create_time, update_user, update_user_id, update_time, version) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, now(), 0) 
    ==> Parameters: title_345_236(String), http://www.test.com/img/category/img-688.gif(String), category description(String), 0(Integer), create user(String), user-337(String), null, null
    。。。
    ==> Parameters: title_534_211(String), http://www.test.com/img/category/img-572.gif(String), category description(String), 0(Integer), create user(String), user-139(String), null, null
    ==> Parameters: title_292_271(String), http://www.test.com/img/category/img-8.gif(String), category description(String), 0(Integer), create user(String), user-547(String), null, null
    testWithExecutorType.BATCH:5283ms

    测试结果发现耗时为:5283ms,而且从打印日志可以看出该方案只一致性一次预编译,之后插入都不需要再次预编译。

    方案3)采用拼接SQL方案:

    用法1:

       @Test
       public void testWithBatchScript() {
          long start = System.currentTimeMillis();
          
          List<ArticleCategory> list = new ArrayList<>();
          for (int i = 0; i < 10000; i++) {
             ArticleCategory articleCategory = new ArticleCategory();
             articleCategory.setTitle("title_" + new Random().nextInt(1000) + "_" + new Random().nextInt(1000));
             articleCategory.setDescription("category description");
             articleCategory.setImgSrc("http://www.test.com/img/category/img-" + new Random().nextInt(1000) + ".gif");
             articleCategory.setState(DataStatus.Living);
             articleCategory.setCreateTime(new Date());
             articleCategory.setCreateUser("create user");
             articleCategory.setCreateUserId("user-" + new Random().nextInt(1000));
    
             list.add(articleCategory);
          }
          articleCategoryMapper.batchInsertsWithScript(list);
    
          long stop = System.currentTimeMillis();
          System.out.println("testWithBatchScript:" + (stop - start) + "ms");
       }

    该方法执行日志:

    JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1b84f475] will not be managed by Spring
    ==>  Preparing: INSERT INTO `article_category` (`title`,`img_src`,`description`,`state`,`create_time`,`create_user`,`create_user_id`,`update_time`,`update_user`,`update_user_id`,`version`) 
    VALUES (?,?,?,?,now(),?,?,now(),?,?,0) , 
    。。。
    (?,?,?,?,now(),?,?,now(),?,?,0) ,
    (?,?,?,?,now(),?,?,now(),?,?,0) 
    ON DUPLICATE KEY UPDATE `update_time` = now() 
    ==> Parameters: 
    title_375_245(String), http://www.test.com/img/category/img-247.gif(String), category description(String), 0(Integer), create user(String), user-930(String), null, null, 
    。。。 
    title_275_39(String), http://www.test.com/img/category/img-875.gif(String), category description(String), 0(Integer), create user(String), user-323(String), null, null, 
    title_743_735(String), http://www.test.com/img/category/img-220.gif(String), category description(String), 0(Integer), create user(String), user-917(String), null, null
    <==    Updates: 10000
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65b104b9]
    testWithBatchScript:3312ms

    测试2:

       @Test
       public void testWithBatchSQL() {
          long start = System.currentTimeMillis();
    
          List<ArticleCategory> list = new ArrayList<>();
          for (int i = 0; i < 10000; i++) {
             ArticleCategory articleCategory = new ArticleCategory();
             articleCategory.setTitle("title_" + new Random().nextInt(1000) + "_" + new Random().nextInt(1000));
             articleCategory.setDescription("category description");
             articleCategory.setImgSrc("http://www.test.com/img/category/img-" + new Random().nextInt(1000) + ".gif");
             articleCategory.setState(DataStatus.Living);
             articleCategory.setCreateTime(new Date());
             articleCategory.setCreateUser("create user");
             articleCategory.setCreateUserId("user-" + new Random().nextInt(1000));
    
             list.add(articleCategory);
          }
          articleCategoryMapper.batchInserts(list);
          long stop = System.currentTimeMillis();
          System.out.println("testWithBatchSQL:" + (stop - start) + "ms");
       }

    执行该方法打印日志:

    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65b104b9] was not registered for synchronization because synchronization is not active
    JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@31ff43be] will not be managed by Spring
    ==>  Preparing: INSERT INTO `article_category`(`title`,`img_src`,`description`,`state`,`create_time`,`create_user`,`create_user_id`,`update_time`,`update_user`,`update_user_id`,`version`)
    VALUES(?,?,?,?,now(),?,?,now(),?,?,0),
    。。。
    (?,?,?,?,now(),?,?,now(),?,?,0),
    (?,?,?,?,now(),?,?,now(),?,?,0) 
    ON DUPLICATE KEY UPDATE `update_time` = now(); 
    ==> Parameters: title_131_603(String), http://www.test.com/img/category/img-831.gif(String), category description(String), 0(Integer), create user(String), user-35(String), null, null, 
    。。。
    title_317_725(String), http://www.test.com/img/category/img-208.gif(String), category description(String), 0(Integer), create user(String), user-968(String), null, null, 
    title_403_241(String), http://www.test.com/img/category/img-870.gif(String), category description(String), 0(Integer), create user(String), user-483(String), null, null
    <==    Updates: 10000
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65b104b9]
    testWithBatchSQL:2978ms

    上边这两种方法实际上原理相同:拼接SQL,一次提交一批SQL方案。

    拼接SQL一次提交方案缺点:当拼接sql超出’max_allowed_packet‘设置大小时,会抛出异常:

    com.mysql.jdbc.PacketTooBigException: Packet for query is too large (8346602 > 4194304(4M)). You can change this value on the server by setting the max_allowed_packet’ variable.

    备注:
    SqlServer 对语句的条数和参数的数量都有限制,分别是 1000 和 2100。
    Mysql 对语句的长度有限制,默认是 4M。
    Mybatis 对动态语句没有数量上的限制。

    解决错误方案:

    我安装的mysql版本是:8.0.17 MySQL Community Server - GPL

    dx:~ $ mysql -uroot -p123456 -h127.0.0.1
    mysql: [Warning] Using a password on the command line interface can be insecure.
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 356
    Server version: 8.0.17 MySQL Community Server - GPL
    
    Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

    该版本的mysql允许的发送给服务器的最大数据包大小情况如下:

    ysql> show variables like '%packet%';
    +---------------------------+------------+
    | Variable_name             | Value      |
    +---------------------------+------------+
    | max_allowed_packet        | 67108864   |
    | mysqlx_max_allowed_packet | 67108864   |
    | slave_max_allowed_packet  | 1073741824 |
    +---------------------------+------------+
    3 rows in set (0.00 sec)

    ax_allowed_packet 设置以字节发送给服务器的最大数据包大小. (默认: 64MB),
    数据库变更需要:大的插入和更新可能会被max_allowed_packet参数限制,导致失败。
    此时,解决方案:
    1)在mysql中执行:set global max_allowed_paclet = 128*1024*1024;
    2)修改mysql服务器的配置(my.ini 或者 my.cnf 文件)参数: max_allowed_packet = 128M 来解决,
    3)需要重启mysql服务器(缺陷)。

    备注:

    上边‘mysqlx_max_allowed_packet’参数时mysqlx插件的配置参数,起作用针对mysqlx起作用,作用于 max_allowed_packet 相同。

    mysql> show plugins;
    +---------------------------------+----------+--------------------+---------+---------+
    | Name                            | Status   | Type               | Library | License |
    +---------------------------------+----------+--------------------+---------+---------+
    | binlog                          | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
    | mysql_native_password           | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
    | sha256_password                 | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
    | caching_sha2_password           | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
    | sha2_cache_cleaner              | ACTIVE   | AUDIT              | NULL    | GPL     |
    | CSV                             | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
    | MEMORY                          | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
    | InnoDB                          | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
    ...
    | mysqlx                          | ACTIVE   | DAEMON             | NULL    | GPL     |
    | mysqlx_cache_cleaner            | ACTIVE   | AUDIT              | NULL    | GPL     |
    +---------------------------------+----------+--------------------+---------+---------+
    44 rows in set (0.01 sec)

    性能总结:

    +----------------------------------------------+------------+
    | 采用方案                                      | 耗时        |
    +----------------------------------------------+------------+
    | 普通处理方法(ExecutorType.SIMPLE)逐条插入处理    | 8s         |
    | 使用ExecutorType.BATCH                        | 5s         |
    | 使用拼接SQL,一致性提交一批SQL                    | 3s         |
    +-----------------------------------------------+------------+
  • 相关阅读:
    3、二进制的秘闻和不同进制间的转换
    Hello World!
    HDU5883 The Best Path(欧拉回路 | 通路下求XOR的最大值)
    Codeforces 722C(并查集 + 思维)
    Floyd 算法求多源最短路径
    vim 配置
    STL容器 -- Vector
    STL容器 -- Bitset
    HDU 5707 Combine String(动态规划)
    HDU 5876 Sparse Graph(补图上BFS)
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/11962714.html
Copyright © 2020-2023  润新知