实现数据批量插入(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等),通过数据分批处理。对于分批处理需要借鉴前面的测试数据来定义分批量的大小,主要是对操作时间调优。