一年前写过一个百万级别数据库数据生成配置xml文件的程序,程序目的是用来把数据库里面的数据生成xml文件.程序可以配置多少文件生成到一个文件中去。
程序刚开始设计的时候说的是最多百万级别数据,最多50W数据生成到一个xml文件里面去,所以在做测试的时候自己也只是造了100W的数据并没有做过多数据量的测试,然后问题就来了....由于程序使用的局点数据量巨大,需要生成xml文件的客户资料接近千万级别的程度,而现场对程序的配置大约是100W条数据生成一个xml文件里面去,程序在这样的大数据量下面偶尔会有崩溃。
最近几天现场催的比较紧,最近抽空把这个问题处理了一下,在解决问题的过程中我把解决的步骤和方法记录了下来,正好和大家共享一下。
<File> <FileType>1</FileType> <RType>12</RType> <Version>03</Version> <BNo>004</BNo> <FileQ>5</FileQ> <FNo>0006</FNo> <RecordNum>1000000</RecordNum> <!-- 上面是文件头 下面是百万个<RecordList> --> <RecordList> <Msisdn>10350719507</Msisdn> <State>1</State> <StartDate>20110303</StartDate> <Date>20110419</Date> <Balance>45000</Balance> </RecordList> ... <!-- 可能百万个 <RecordList> 块--> </File>
比较好用的方法是使用开源框架,比如XStream 直接把javabean 生成 xml
自己做的一个可以使用极少的内存生成无限制大的xml文件框架由3部分生成xml文件
例如:xxx.toXML(Object obj, String fileName)
例如:xxx.appendXML(Object object); //object 可以是ArrayList 或者一个单独的javaBean
例如:xxx.finishXML();
有了思路之后,大家可以尝试着自己写一个类似的大数据处理框架(千万级别以上),如何有什么需要帮助的可以直接联系我,因为是公司的程序,不太敢放出来,怕......
根据文件崩溃时候的日志发现是在生成xml的框架里面报的错误,第一想到的是框架有些资源没有释放.于是把自己做的文件生成框架整体的排查了一遍,并且自己写个简单程序生成200万条数据,使用xml框架生成一个xml文件,整个生成过程中任务管理器(xp)查看程序对应的java进程使用的内存基本在20M左右,因此排除框架的问题.怀疑是数据库查询和调用框架的部门出现问题.
检测了一遍主程序的关键部分代码,优化了一下字符串处理.手动的释放一些对象的内存(例如:调用ArrayList.clear(),或者把对象置空等),分配512内存后运行程序,60万数据的时候内存溢出,因为能主动释放的对象都已经释放掉了,还是没有解决,果断放弃看代码,准备使用JProfile进行内存检测.
通过在数据库中生成300W条数据,在JProfile上面多跑程序,一边运行,一边调用JProfile 提供的执行GC按钮主动运行垃圾回收,运行50W数据后,通过检测中发现 java.long.String[] 和 oracle.jdbc.driver.Binder[] 两个对象的数目一直保持在自增状态,而且数目基本上差不多,对象数目 都在200W以上,由于java.long.String[]对象是需要依赖对象而存在的,因此断定问题就出在oracle.jdbc.driver.Binder[]上面,由于改对象存在引用导致String[]不能正常回收.
检测到oracle.jdbc.driver.Binder 被 oracle.jdbc.driver.T4CPreparedStatement 引起,而T4CPreparedStatement正好是Oracle对jdbc OraclePreparedStatement的具体实现,因此断定是在数据库处理方面出现的问题导致oracle.jdbc.driver.Binder对象不能正常释放,通过再一次有目的的检测代码,排查jdbc数据查询的问题,把问题的矛头直至数据库的批处理和事务处理.因此程序是每生成一个文件成功后,会把已经处理的数据转移到对应的历史表中进行备份,而再个表操作的过程中使用了批处理和事务,使用批处理主要是保证执行速度,使用事务主要是保证同时成功和失败。
所以准备监控oracle.jdbc.driver.Binder的对象数目是否和查询次数对应.,通过在程序中Sysout输出查询次数 + JProfile运行GC测试 Binder,数据匹配,证实是java在数据库批处理的过程中有些问题.
原因如下:100W数据生成一个文件的过程中,等文件生成完毕之后才能把数据库中的数据备份到历史表中,这个时候才能进行事务的提交,也就是执行commit(), 并且删除原表数据,100W数据按照3000一批写入文件,每批次只是通过 PreparedStatement.addBatch();加入到批次里面去,并没有执行PreparedStatement.executeBatch(),而是在commit()之前统一调用的PreparedStatement.executeBatch(),这样的话PreparedStatement就会缓存100W条数据信息,造成了内存溢出.
try{ conn.setAutoCommit(false); pst = conn.prepareStatement(insertSql); pstDel = conn.prepareStatement(delSql); pstUpdate = conn.prepareStatement(sql); ... //totalSize = 100W数据 / 3000一批次 for (int i = 1; i <= totalSize; i++) { client.appendXML(list); } // 错误的使用方法 client.finishXML(); pst.executeBatch(); pstDel.executeBatch(); } ... finally { try { if (isError) { conn.rollback(); } else conn.commit(); ... } ... }
正确的方法如下
try{ conn.setAutoCommit(false); pst = conn.prepareStatement(insertSql); pstDel = conn.prepareStatement(delSql); pstUpdate = conn.prepareStatement(sql); ... //totalSize = 100W数据 / 3000一批次 for (int i = 1; i <= totalSize; i++) { list = 从数据库中查询3000条数据 client.appendXML(list); pst.executeBatch(); pstDel.executeBatch(); } client.finishXML(); } ... finally { try { if (isError) { conn.rollback(); } else conn.commit(); ... } ... }
如果碰到和我一样的需要给大家一个提醒。
oracle在每次执行executeBatch();进行批处理的时候,当前connection对应的rownum会根据操作的结果发生变化。
在执行pst.executeBatch(); 之后,当前连接的 rownum 数就会发生变化. 因此凡是通过rownum查询数据的程序都要小心这一点。
最后,欢迎做Java的工程师朋友们加入Java高级架构进阶Qqun:963944895
群内有技术大咖指点难题,还提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰, 我们必须不断学习,否则我们将被学习者超越!
趁年轻,使劲拼,给未来的自己一个交代!