嵌入式 SQL Firebird 使用 Java SE 批量更新 OutOfMemoryError

Posted

技术标签:

【中文标题】嵌入式 SQL Firebird 使用 Java SE 批量更新 OutOfMemoryError【英文标题】:Embedded SQL Firebird batch update OutOfMemoryError with Java SE 【发布时间】:2012-12-07 00:06:41 【问题描述】:

我在使用嵌入式 firebird 数据库引擎和 Java SE 时遇到了一个大问题。 我目前正在开发一种过滤工具,供用户过滤数据。 所以我做了两个过滤选项,用户可以选择一个或两个:

    从黑名单中过滤(黑名单由用户控制)。 根据记录上传和过滤的每条记录的海量列表进行过滤。

用户上传的数据以纯文本逗号或令牌分隔,如下所示:

(SET OF COLUMNS)| RECORD TO FILTER |
0-MANY COLUMNS  |       ABC2       |
0-MANY COLUMNS  |       ABC5       |

当我将它上传到数据库时,我为每个过滤器添加一个 FLAG

(SET OF COLUMNS) | RECORD TO FILTER | FLAG FOR FIlTER A | FLAG FOR FILTER B  |
0-MANY COLUMNS   |       ABC2       |                   |                    |
0-MANY COLUMNS   |       ABC5       |                   |                    | 

因此,当涉及到第二个过滤器时,程序在第一次运行软件时有一个主空表,然后它用第一次上传的所有记录填充该表。 在用户进行几次文本上传后,主表将具有如下表的唯一记录:

 Record |      Date criteria for filtering      |
 ABC1   | 08/11/2012:1,07/11/2012:3,06/11/2012:5|
 ABC2   | 05/11/2012:1,04/11/2012:0,03/11/2012:0|
 ABC3   | 12/11/2012:3,11/11/2012:0,10/11/2012:0|
 ABC4   | 12/11/2012:1,11/11/2012:0,10/11/2012:0|
 ABC5   | 12/11/2012:3,11/11/2012:0,10/11/2012:3|
 ABC9   | 11/11/2012:3,10/11/2012:1,09/11/2012:0|

处理数据时,例如,软件同时更新主表和用户表:

(SET OF COLUMNS| RECORD TO FILTER | FLAG FOR FIlTER A | FLAG FOR FILTER B  |
0-MANY COLUMNS |       ABC4       |                   |                    | 
0-MANY COLUMNS |       ABC9       |                   |                    | 

所以主表会更新:

 Record |      Day criteria for filtering      |
 ABC1   | 08/11/2012:1,07/11/2012:3,06/11/2012:5|
 ABC2   | 05/11/2012:1,04/11/2012:0,03/11/2012:0|
 ABC3   | 12/11/2012:3,11/11/2012:0,10/11/2012:0|
 ABC4   | 12/11/2012:1,11/11/2012:0,10/11/2012:0| ->12/11/2012:2,11/11/2012:0,10/11/2012:0
 ABC5   | 12/11/2012:3,11/11/2012:0,10/11/2012:3|
 ABC9   | 11/11/2012:3,10/11/2012:1,09/11/2012:0| ->12/11/2012:1,11/11/2012:3,10/11/2012:1

如果在过去三天内数据条件事件达到四个以上,用户表将标记过滤器 B。请注意,每个日期旁边都有一个整数。

(SET OF COLUMNS)| RECORD TO FILTER | FLAG FOR FIlTER A | FLAG FOR FILTER B  |
 0-MANY COLUMNS |       ABC4       |                   |                    | 
 0-MANY COLUMNS |       ABC9       |                   |          X         | 

两个更新都在一个事务中,问题是当用户上传超过 800,000 条记录时,我的程序在 while 循环中抛出以下异常。 我使用 StringBuilder 解析和附加方法来获得可变天数字符串的最大性能。

java.lang.OutOfMemoryError: Java 堆空间

这是我的代码,我用了五天:

FactoriaDeDatos factoryInstace = FactoriaDeDatos.getInstance();
Connection cnx = factoryInstace.getConnection();
cnx.setAutoCommit(false);
PreparedStatement pstmt = null;
ResultSet rs=null;
pstmt = cnx.prepareStatement("SELECT CM.MAIL,CM.FECHAS FROM TCOMERCIALMAIL CM INNER JOIN TEMPMAIL TMP ON CM.MAIL=TMP."+colEmail);
rs=pstmt.executeQuery();
pstmtDet = cnx.prepareStatement("ALTER INDEX IDX_MAIL INACTIVE");
pstmtDet.executeUpdate();
pstmtDet = cnx.prepareStatement("SET STATISTICS INDEX IDX_FECHAS");
pstmtDet.executeUpdate();
pstmtDet = cnx.prepareStatement("ALTER INDEX IDX_FECHAS INACTIVE");
pstmtDet.executeUpdate();
pstmtDet = cnx.prepareStatement("SET STATISTICS INDEX IDX_FECHAS");
pstmtDet.executeUpdate();
sql_com_local_tranx=0;
int trxNum=0;
int ix=0;
int ixE1=0;
int ixAc=0;
StringBuilder sb;
StringTokenizer st;
String fechas;
int pos1,pos2,pos3,pos4,pos5,pos6,pos7,pos8,pos9;
StringBuilder s1,s2,sSQL,s4,s5,s6,s7,s8,s9,s10;
long startLoop = System.nanoTime();

long time2 ;
boolean ejecutoMax=false;
//int paginador_sql=1000;
//int trx_ejecutada=0;
sb=new StringBuilder();
s1=new StringBuilder();
s2=new StringBuilder();
sSQL=new StringBuilder();
s4=new StringBuilder();
s6=new StringBuilder();
s8=new StringBuilder();
s10=new StringBuilder();
while(rs.next())
   //De aqui
   actConteoDia=0;
   sb.setLength(0);
   sb.append(rs.getString(2));
   pos1= sb.indexOf(":",0);  
   pos2= sb.indexOf(",",pos1+1);  
   pos3= sb.indexOf(":",pos2+1);  
   pos4= sb.indexOf(",",pos3+1);  
   pos5= sb.indexOf(":",pos4+1);
   pos6= sb.indexOf(",",pos5+1);
   pos7= sb.indexOf(":",pos6+1);
   pos8= sb.indexOf(",",pos7+1);
   pos9= sb.indexOf(":",pos8+1);
   s1.setLength(0);
   s1.append(sb.substring(0, pos1));
   s2.setLength(0);
   s2.append(sb.substring(pos1+1, pos2));
   s4.setLength(0);
   s4.append(sb.substring(pos3+1, pos4));
   s6.setLength(0);
   s6.append(sb.substring(pos5+1, pos6));
   s8.setLength(0);
   s8.append(sb.substring(pos7+1, pos8));
   s10.setLength(0);
   s10.append(sb.substring(pos9+1));
   actConteoDia=Integer.parseInt(s2.toString());
   actConteoDia++;
   sb.setLength(0);
   //sb.append(s1).a
   if(actConteoDia>MAXIMO_LIMITE_POR_SEMANA)
      actConteoDia=MAXIMO_LIMITE_POR_SEMANA+1;
   
   sb.append(s1).append(":").append(actConteoDia).append(",").append(rs.getString(2).substring(pos2+1, rs.getString(2).length()));
   //For every date record it takes aprox 8.3 milisec by record

   sSQL.setLength(0);
   sSQL.append("UPDATE TCOMERCIALMAIL SET FECHAS='").append(sb.toString()).append("' WHERE MAIL='").append(rs.getString(1)).append("'");

   pstmtDet1.addBatch(sSQL.toString());
   //actConteoDia=0;
   //actConteoDia+=Integer.parseInt(s2.toString());
   actConteoDia+=Integer.parseInt(s4.toString());
   actConteoDia+=Integer.parseInt(s6.toString());
   actConteoDia+=Integer.parseInt(s8.toString());
   actConteoDia+=Integer.parseInt(s10.toString());
   if(actConteoDia>MAXIMO_LIMITE_POR_SEMANA)
      sSQL.setLength(0);
      sSQL.append("UPDATE TEMPMAIL SET DIASLIMITE='S' WHERE ").append(colEmail).append("='").append(rs.getString(1)).append("'");
      pstmtDet.addBatch(sSQL.toString());
   

   sql_com_local_tranx++;

   if(sql_com_local_tranx%2000==0 || sql_com_local_tranx%7000==0  )
      brDias.setString("PROCESANDO "+sql_com_local_tranx);
      pstmtDet1.executeBatch();
      pstmtDet.executeBatch();

   
   if(sql_com_local_tranx%100000==0)
       System.gc();
       System.runFinalization();
   


pstmtDet1.executeBatch();
pstmtDet.executeBatch();
cnx.commit();

我进行了遥测测试,以便追踪问题所在。 我认为这是个大问题,但我不知道问题究竟出在哪里。 我正在添加一些遥测测试的图像,请我正确解释它们。

gc 与 jvm 保持对象存活的时间成反比:

http://imageshack.us/photo/my-images/849/66780403.png

内存堆从 50 MB 变为 250 MB,使用的堆达到 250 MB,从而产生 outOfMemory 异常:

50 MBhttp://imageshack.us/photo/my-images/94/52169259.png

达到 250 MBhttp://imageshack.us/photo/my-images/706/91313357.png

内存不足http://imageshack.us/photo/my-images/825/79083069.png

LiveBytes 排序生成的最终对象栈:

http://imageshack.us/photo/my-images/546/95529690.png

我们将不胜感激任何帮助、建议和回答。

【问题讨论】:

【参考方案1】:

问题是您正在使用PreparedStatement,就好像它是Statement,因为您正在调用addBatch(string)。 javadoc of this method 说:

注意:不能在 PreparedStatement 或 CallableStatement 上调用此方法。

此注释是在 JDBC 4.0 中添加的,在此之前它表示该方法是可选的。 Jaybird 允许您在 PreparedStatement 上调用此方法的事实是一个错误:我在 Jaybird 跟踪器中创建了问题 JDBC-288。

现在到 OutOfMemoryError 的原因:当您在 Jaybird (FBPreparedStatement) 的 PreparedStatement 实现上使用 addBatch(String) 时,它会添加到 Statement 实现 (FBStatement) 的内部列表中)。对于FBStatement,当你调用executeBatch()时,它会执行这个列表中的所有语句,然后清除它。在FBPreparedStatement 中,executeBatch() 被覆盖以使用批处理参数执行最初准备好的语句(在您的示例中,它不会做任何事情,因为您实际上从未添加 PreparedStatement 样式的批处理)。它会从不执行您使用addBatch(String) 添加的语句,但它也会不清楚FBStatement 中的语句列表,这很可能是您的OutOfMemoryError.

基于此,解决方案应该是使用 cnx.createStatement 创建一个 Statement 并使用它来执行您的查询,或者调查您是否可以从使用一个或多个带有参数化查询的 PreparedStatement 对象中受益。看起来您应该能够使用两个单独的 PreparedStatement,但我不是 100% 确定;额外的好处将是防止 SQL 注入和轻微的性能改进。

附录

这个问题已经修复since Jaybird 2.2.2

全面披露:我是 Jaybird / Firebird JDBC 驱动程序的开发者。

【讨论】:

感谢您在 Firebird 跟踪器上创建问题。我无法从我的工作中访问它._。我会根据您的建议解决我的问题,谢谢!!! 好的...所以,我已将准备好的语句对象更改为 Statement 对象,但更新时间非常缓慢,而且它只是在 while 循环中运行两个更新之一,因为记录没有在上传的用户表上达到了更新的标准,所以在极端情况下它会慢一倍。当您说“它永远不会执行您使用 addBatch(String) 添加的语句”时,我真的不明白为什么?您的意思是,使用 Statement 对象,表实际上以如此缓慢的速度更新? 我的意思是,当您使用PreparedStatement 时,使用addBatch(String) 提交的查询永远不会执行。顺便说一句:定义慢:您一次执行 2000 个查询,并且查看您每 100000 行请求一次垃圾收集的事实,我假设我们不是在谈论少量查询。 RDB$DB_KEY 作为行标识符,但我认为这不会更快,并且在您的情况下它没有用,因为它们在事务之外不稳定。使用多个线程进行更新可能会加快一点速度(但同一张表上的太多线程会由于锁争用等而减慢速度)但据我所知,插入和更新之间的区别主要与 MVCC 有关火鸟的架构。 @jRam90 我已经提交了一个修复程序,以禁止在 PreparedCallableStatement 中调用这些方法:sourceforge.net/p/firebird/code/57463 Jaybird 2.2.2 可能会在一月份发布。【参考方案2】:

在遍历结果集时不要执行批处理语句。将要执行的 sql 存储在集合中,然后在处理完结果集后 开始执行新的sql。是否所有事情都必须发生在同一个事务中?

【讨论】:

嗯,不一定。不错的方法,我会试试的。问题是,如何在内存集合中保持 2 000 000 次更新?我试过使用数组,但内存堆不能接受这么多的字符串。 在添加批处理时迭代结果集应该不是问题。我看到的潜在问题是使用PreparedStatement,就好像它是Statement。看起来 Jaybird 在执行后没有清除批处理语句列表;今晚我会检查代码。 BevynQ 你是对的,将结果集迭代到一个临时的 .txt 文件中,然后使用文本文件作为源来迭代批量更新,这样会更快。我必须再次声明,使用带有索引的 PLAN 语句并将更新限制为只有一个有帮助,根据 Mark Rotteveel,使用 Stament 对象是进行批量更新的唯一方法。 理想情况下,您可以在更新命令中执行此操作,但 2000000 行对于数据库来说可能有点过多。系统通常会以可重新启动的块的形式进行此类更新。不知道这是否适合您。 您可以考虑使用用户定义的函数或触发器来执行字符串操作。批处理语句也依赖于驱动程序,并非所有驱动程序都支持它们(有些似乎是作弊)。

以上是关于嵌入式 SQL Firebird 使用 Java SE 批量更新 OutOfMemoryError的主要内容,如果未能解决你的问题,请参考以下文章

在 .NET 中使用嵌入式 firebird 数据库

Firebird的主要信息

使用嵌入式 Firebird 时出错

使用 FireDAC 连接到嵌入式 Firebird 2.5

如何使用 Visual C# 2010 连接和使用 Firebird db 嵌入式服务器

FireBird 嵌入式服务器问题