mybatis批量导入

实现数据批量插入(jdbc\mybatis)

1. 实现方式 – JDBC:

  • 普通方式插入for循环
 1    private String url = "jdbc:mysql://localhost:3306/test01";
 2     private String user = "root";
 3     private String password = "123456";
 4     @Test
 5     public void Test(){
 6         Connection conn = null;
 7         PreparedStatement pstm =null;
 8         ResultSet rt = null;
 9         try {
10             Class.forName("com.mysql.jdbc.Driver");
11             conn = DriverManager.getConnection(url, user, password);        
12             String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)";
13             pstm = conn.prepareStatement(sql);
14             Long startTime = System.currentTimeMillis();
15             Random rand = new Random();
16             int a,b,c,d;
17             for (int i = 1; i <= 1000; i++) {
18                     pstm.setInt(1, i);
19                     pstm.setInt(2, i);
20                     a = rand.nextInt(10);
21                     b = rand.nextInt(10);
22                     c = rand.nextInt(10);
23                     d = rand.nextInt(10);
24                     pstm.setString(3, "188"+a+"88"+b+c+"66"+d);
25                     pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d);27                     pstm.executeUpdate();
28             }
29             Long endTime = System.currentTimeMillis();
30             System.out.println("OK,用时:" + (endTime - startTime)); 
31         } catch (Exception e) {
32             e.printStackTrace();
33             throw new RuntimeException(e);
34         }finally{
35             if(pstm!=null){
36                 try {
37                     pstm.close();
38                 } catch (SQLException e) {
39                     e.printStackTrace();
40                     throw new RuntimeException(e);
41                 }
42             }
43             if(conn!=null){
44                 try {
45                     conn.close();
46                 } catch (SQLException e) {
47                     e.printStackTrace();
48                     throw new RuntimeException(e);
49                 }
50             }
51         }
52     }

  • 使用事务方式插入
    先将命令的提交方式设为false,即手动提交conn.setAutoCommit(false);最后在所有命令执行完之后再提交事务conn.commit();
 private String url = "jdbc:mysql://localhost:3306/test01";
 2     private String user = "root";
 3     private String password = "123456";
 4     @Test
 5     public void Test(){
 6         Connection conn = null;
 7         PreparedStatement pstm =null;
 8         ResultSet rt = null;
 9         try {
10             Class.forName("com.mysql.jdbc.Driver");
11             conn = DriverManager.getConnection(url, user, password);        
12             String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)";
13             pstm = conn.prepareStatement(sql);
14             conn.setAutoCommit(false);
15             Long startTime = System.currentTimeMillis();
16             Random rand = new Random();
17             int a,b,c,d;
18             for (int i = 1; i <= 100000; i++) {
19                     pstm.setInt(1, i);
20                     pstm.setInt(2, i);
21                     a = rand.nextInt(10);
22                     b = rand.nextInt(10);
23                     c = rand.nextInt(10);
24                     d = rand.nextInt(10);
25                     pstm.setString(3, "188"+a+"88"+b+c+"66"+d);
26                     pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d);
27                     pstm.executeUpdate();
28             }
29             conn.commit();
30             Long endTime = System.currentTimeMillis();
31             System.out.println("OK,用时:" + (endTime - startTime)); 
32         } catch (Exception e) {
33             e.printStackTrace();
34             throw new RuntimeException(e);
35         }finally{
36             if(pstm!=null){
37                 try {
38                     pstm.close();
39                 } catch (SQLException e) {
40                     e.printStackTrace();
41                     throw new RuntimeException(e);
42                 }
43             }
44             if(conn!=null){
45                 try {
46                     conn.close();
47                 } catch (SQLException e) {
48                     e.printStackTrace();
49                     throw new RuntimeException(e);
50                 }
51             }
52         }
53     }
  • 批量处理
    首先,JDBC连接的url中要加rewriteBatchedStatements参数设为true是批量操作的前提,其次就是检查mysql驱动包时候是5.1.13以上版本(低于该版本不支持)
private String url = "jdbc:mysql://localhost:3306/test01?rewriteBatchedStatements=true";
 2     private String user = "root";
 3     private String password = "123456";
 4     @Test
 5     public void Test(){
 6         Connection conn = null;
 7         PreparedStatement pstm =null;
 8         ResultSet rt = null;
 9         try {
10             Class.forName("com.mysql.jdbc.Driver");
11             conn = DriverManager.getConnection(url, user, password);        
12             String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)";
13             pstm = conn.prepareStatement(sql);
14             Long startTime = System.currentTimeMillis();
15             Random rand = new Random();
16             int a,b,c,d;
17             for (int i = 1; i <= 100000; i++) {
18                     pstm.setInt(1, i);
19                     pstm.setInt(2, i);
20                     a = rand.nextInt(10);
21                     b = rand.nextInt(10);
22                     c = rand.nextInt(10);
23                     d = rand.nextInt(10);
24                     pstm.setString(3, "188"+a+"88"+b+c+"66"+d);
25                     pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d);
26                     pstm.addBatch();
27             }
28             pstm.executeBatch();
29             Long endTime = System.currentTimeMillis();
30             System.out.println("OK,用时:" + (endTime - startTime)); 
31         } catch (Exception e) {
32             e.printStackTrace();
33             throw new RuntimeException(e);
34         }finally{
35             if(pstm!=null){
36                 try {
37                     pstm.close();
38                 } catch (SQLException e) {
39                     e.printStackTrace();
40                     throw new RuntimeException(e);
41                 }
42             }
43             if(conn!=null){
44                 try {
45                     conn.close();
46                 } catch (SQLException e) {
47                     e.printStackTrace();
48                     throw new RuntimeException(e);
49                 }
50             }
51         }
52     }
  • 事务操作+批量处理方式
 private String url = "jdbc:mysql://localhost:3306/test01?rewriteBatchedStatements=true";
 2     private String user = "root";
 3     private String password = "123456";
 4     @Test
 5     public void Test(){
 6         Connection conn = null;
 7         PreparedStatement pstm =null;
 8         ResultSet rt = null;
 9         try {
10             Class.forName("com.mysql.jdbc.Driver");
11             conn = DriverManager.getConnection(url, user, password);        
12             String sql = "INSERT INTO userinfo(uid,uname,uphone,uaddress) VALUES(?,CONCAT('姓名',?),?,?)";
13             pstm = conn.prepareStatement(sql);
14             conn.setAutoCommit(false);
15             Long startTime = System.currentTimeMillis();
16             Random rand = new Random();
17             int a,b,c,d;
18             for (int i = 1; i <= 100000; i++) {
19                     pstm.setInt(1, i);
20                     pstm.setInt(2, i);
21                     a = rand.nextInt(10);
22                     b = rand.nextInt(10);
23                     c = rand.nextInt(10);
24                     d = rand.nextInt(10);
25                     pstm.setString(3, "188"+a+"88"+b+c+"66"+d);
26                     pstm.setString(4, "xxxxxxxxxx_"+"188"+a+"88"+b+c+"66"+d);
27                     pstm.addBatch();
28             }
29             pstm.executeBatch();
30             conn.commit();
31             Long endTime = System.currentTimeMillis();
32             System.out.println("OK,用时:" + (endTime - startTime)); 
33         } catch (Exception e) {
34             e.printStackTrace();
35             throw new RuntimeException(e);
36         }finally{
37             if(pstm!=null){
38                 try {
39                     pstm.close();
40                 } catch (SQLException e) {
41                     e.printStackTrace();
42                     throw new RuntimeException(e);
43                 }
44             }
45             if(conn!=null){
46                 try {
47                     conn.close();
48                 } catch (SQLException e) {
49                     e.printStackTrace();
50                     throw new RuntimeException(e);
51                 }
52             }
53         }
54     }

2 . 实现方式 – mybatis
mybatis中的foreach循环

<insert id="batchInsert" parameterType="java.util.List">
    insert into USER (id, name) values
    <foreach collection="list" item="model" index="index" separator=","> 
        (#{model.id}, #{model.name})
    </foreach>
</insert>

其原理是将传统的单条insert语句

INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转换为:

INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2"),
                                                 ("data1", "data2"),
                                                 ("data1", "data2"),
                                                 ("data1", "data2"),
                                                 ("data1", "data2");

理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。
乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。
mybatis默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。
所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。
重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看 http://www.mybatis.org/mybatis-dynamic-sql/docs/insert.html 中 Batch Insert Support 标题里的内容)


SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
    List<SimpleTableRecord> records = getRecordsToInsert(); // not shown
 
    BatchInsert<SimpleTableRecord> batchInsert = insert(records)
            .into(simpleTable)
            .map(id).toProperty("id")
            .map(firstName).toProperty("firstName")
            .map(lastName).toProperty("lastName")
            .map(birthDate).toProperty("birthDate")
            .map(employed).toProperty("employed")
            .map(occupation).toProperty("occupation")
            .build()
            .render(RenderingStrategy.MYBATIS3);
 
    batchInsert.insertStatements().stream().forEach(mapper::insert);
 
    session.commit();
} finally {
    session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。


Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
        "insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
    ps.setString(1,name);
    ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。

总结一下,如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用 的插入的话,需要将每次插入的记录控制在 20~50 左右。

3. 方案
因为要考虑JVM的GC所以数据应该限制一下,但鉴于Mybatis大数据量的批量插入效率不高,所以根据数据大小分段治理。

3.1 小于1W使用:Mybatis批量插入方案
对JVM进行调优,但主要的性能瓶颈在批量插入操作。鉴于mybatis在项目开发方面的优势,数据量很小的情况下还是建议使用Mybatis。

3.2 大于1W小于10W使用:JDBC批量+事务处理
对JVM进行调优(设置Stack和GC等)。一般操作30秒以内是可以容忍的性能耗时。

3.3 10W以上数据使用:数据分批+JDBC批量+事务处理
对JVM进行调优(设置Stack和GC等),通过数据分批处理。对于分批处理需要借鉴前面的测试数据来定义分批量的大小,主要是对操作时间调优。

猜你喜欢

转载自blog.csdn.net/tangdou2333/article/details/93409037