jdbc插入百万级数据

素颜马尾好姑娘i 2021-10-15 11:03 490阅读 0赞

1.背景介绍

快速插入百万级数据

我们一般用JDBC向数据库插入数据时,是用statement或preparedstatment中的update或execute方法来实现,但当要插入的数据为百万级别甚至千万级别时,这种方法显然无法满足我们的需求,可以尝试写一个循环,循环以百万次statement.excute(sql)方法,我这边测试是半个小时还没有插完。

缺点:一条一条的sql执行,大部分时间都是消耗在打开数据库,关闭数据库上了。
是否可以只发一句sql而实现批量插入呢

为此jdbc也提供了相应的方法来应对这种场景。今天就来介绍jdbc批处理的相关api,当然如果今天仅仅是介绍api,那大家大可百度就好了
这次分享会除了介绍api,更重要的是介绍些我踩过的坑以及对此相关的一些见解。

2.知识剖析

批量处理

|— Statement
— PreparedStatement 子接口,建议使用,会对sql语句先进行编译再给数据库
PreparedStatement对象不仅包含了SQL语句,而且大多数情况下这个语句已经被预编译过,因而当其执行时,只需DBMS运行SQL语句,而不必先编译。
当你需要执行Statement对象多次的时候,PreparedStatement对象将会大大降低运行时间,当然也加快了访问数据库的速度。

批处理相关方法
void addBatch(String sql) 添加批处理
void clearBatch() 清空批处理
int[] executeBatch() 执行批处理

事务提交
con.setAutoCommit(false); 默认自动提交,在这里需要设置为false,手动提交
con.commit();
con.rollback();

3.常见问题
批量处理无法生效
1、rewriteBatchedStatements=true
2、mysql 5.1.13

4.编码实战

  1. @Test
  2. public void testForRemote() throws Exception {
  3. try {
  4. //准白sql语句
  5. String sql = "INSERT INTO table1(NAME,gender,age,qq) VALUES(?,?,?,?)";
  6. Connection con = null;
  7. //加载驱动
  8. Class.forName("com.mysql.jdbc.Driver");
  9. //获取到远程服务器的连接
  10. con = DriverManager.getConnection("jdbc:mysql://localhost:3306/Jnshu1?useUnicode=true&"+
  11. "characterEncoding=utf-8&useSSL=false&rewriteBatchedStatements=true",
  12. "root","root");
  13. //设置非自动提交事务
  14. con.setAutoCommit(false);
  15. PreparedStatement pstat = con.prepareStatement(sql);
  16. //获得模拟数据
  17. list2 = getPersonWithArg();
  18. System.out.println(list2.size());
  19. long start = System.currentTimeMillis();
  20. for (int i=0; i<list2.size(); i++) {
  21. Person person = list2.get(i);
  22. pstat.setString(1, person.getNAME());
  23. pstat.setString(2, person.getGender());
  24. pstat.setString(3, person.getAge());
  25. pstat.setString(4, person.getQq());
  26. //10w提交一次
  27. pstat.addBatch();
  28. if(i % 100000 == 0){
  29. pstat.executeBatch();
  30. pstat.clearBatch();
  31. }
  32. }
  33. pstat.executeBatch(); //执行批处理
  34. pstat.clearBatch(); //清空批处理
  35. con.commit();
  36. long end = System.currentTimeMillis();
  37. pstat.close();
  38. con.close();
  39. System.out.print((end-start)/1000+"秒。");
  40. } catch (ClassNotFoundException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. // 模拟数据
  45. //1000万数据执行后会内存溢出
  46. public List<Person> getPersonWithArg(){
  47. //由一个StringBuffer来代替之前4个String的反复字符串操作。
  48. StringBuffer sb = new StringBuffer("");// String = "";
  49. for (int r = 0; r < 1000000 ; r++) {
  50. Person person = new Person();
  51. //随机产生姓名
  52. for (int i = 0; i < 3; i++) {
  53. sb.append((char) (0x4e00 + (Math.random() * (0x9fa5 - 0x4e00 + 1))));
  54. }
  55. person.setNAME(sb.toString());
  56. //用完后清楚里面的内容,以供下一次使用
  57. sb.delete(0,sb.length());
  58. //随机产生性别
  59. int sexf = (int)(Math.random() * 2);//[0,2)
  60. if(sexf==0){
  61. sb.append("男");
  62. }else if( sexf == 1){
  63. sb.append("女");
  64. }else {
  65. sb.append("Unknow");
  66. }
  67. person.setGender(sb.toString());
  68. sb.delete(0,sb.length());
  69. //随机产生年龄
  70. int d;
  71. for (int i = 0; i < 2; i++) {
  72. d = (int)(Math.random()*10);
  73. sb.append(d);
  74. }
  75. person.setAge(sb.toString());
  76. sb.delete(0,sb.length());
  77. // 随机产生qq
  78. int j;
  79. for (int i = 0; i < 10; i++) {
  80. d = (int)(Math.random()*10);
  81. sb.append(d);
  82. }
  83. person.setQq(sb.toString());
  84. sb.delete(0,sb.length());
  85. list.add(person);
  86. }
  87. return list;
  88. }

5.扩展思考

  1. 批处理addbatch为什么比一般插入效率高,难道仅仅因为名字叫批处理?
    答案:当然不是,如果这样的话我们大可再搞一个超级批处理叫addsuperbatch,
    其实我们大可做一个这样的实验,在sql语句后面加一个 ; ,看程序是否可以正常运行
    我们知道,如果不是批处理加一个分号是很正常的,可以正常执行的。那么现在我这里做个实验
    可以看到,他这里说你的SQL语句存在语法问题,在几几行附近我们检查把它复制到命令行去执行,发现是没有任何问题的。没错,问题就出现在分号上,关于分号问题
    一般的sql语句是要分号结尾的,平时的一般插入加与不加都可,你不加系统会自动给你加上,
    但批处理不一样,它会对你的sql语句进行加工然后发送给mysql服务端执行加工后的语句,
    可以看到我这里写的是“INSERT INTO table1(NAME,gender,age,qq) VALUES(?,?,?,?)”
    如果把这条语句如实的发给数据库,即便把里面的问号传好了相关参数,也是不可能执行上百万条语句的,
    要知道原因,这里先给介绍一个sql语句,(打开navicat展示),可以看到这条语句后面还可以
    随便加,加几十万条都可以。那么答案自然出来了,它会把我们的“INSERT INTO table1(NAME,gender,age,qq) VALUES(?,?,?,?)”
    封装成“INSERT INTO table1(NAME,gender,age,qq) VALUES(?,?,?,?),( ∞ ,∞ )”,
    即括号内是参数组,多个参数组用逗号隔开,
    这是符合mysql语法的,
    如果你加了分号,就变成了“INSERT INTO table1(NAME,gender,age,qq) VALUES(?,?,?,?);,( ∞ ,∞ );”
    那么综上我们可以知道,批处理比一般的插入快,原因就在于批处理只发送了一条sql过去,数据库只开关一次
    即可,而一般插入如果插100万要开关数据库100万次,大大的开销就在这里。而一条大体量的批处理sql语句,
    尽管文本长,但数据库对文本处理的性能差异是毫秒级的,而不停的开关数据库是秒级别的,所以一般方式插入百万数据可以
    可以插一个小时,有时候甚至中间出个错会卡机。

2.批处理时,每批不同数量sql是否对性能有影响(上数据。5w/10w最佳)

进入正式测试,100万数据插入

100万整个一批 27s

50万 26s

20万 28

10万 19s

5万一批处理 18s

1万 28s

5000 48s

由上面可知,批处理的速度跟每批处理数据量有关,每次处理太少会慢,但太高也不好,经过上面的测试对于100万的插入每批10万或5万最佳。

3.插入1000w时会怎么样?

很可能会内存溢出,这边测试如果使用直接插入属性方法没事,如果采用我这里的产生
1000w个对象的方式,测试过几次溢出率100%,
java.lang.OutOfMemoryError: Java heap space

发表评论

表情:
评论列表 (有 0 条评论,490人围观)

还没有评论,来说两句吧...

相关阅读

    相关 jdbc进行数据插入

    最近没事比较了下jdbc数据插入大概400万的到mysql数据库,用了两种方式 一种是直接使用原生的jdbc插入数据到数据库,一种是采用调用数据库存储过程的方式

    相关 java导出数据

    JAVA 实现大数据量导出操作时,如果采用POI直接导出,会出现内存溢出的情况。再者EXCEL的SHEET也存在行数的限制,Excel2003版最大行数是655536行、Exc