java千万级别数据生成文件思路和优化

来源:互联网 发布:mac安装myeclipse 编辑:程序博客网 时间:2024/05/21 09:25

java千万级别数据生成文件思路和优化

一年前写过一个百万级别数据库数据生成配置xml文件的程序,程序目的是用来把数据库里面的数据生成xml文件.程序可以配置多少文件生成到一个文件中去.

    程序刚开始设计的时候说的是最多百万级别数据,最多50W数据生成到一个xml文件里面去,所以在做测试的时候自己也只是造了100W的数据并没有做过多数据量的测试,然后问题就来了....由于程序使用的局点数据量巨大,需要生成xml文件的客户资料接近千万级别的程度,而现场对程序的配置大约是100W条数据生成一个xml文件里面去,程序在这样的大数据量下面偶尔会有崩溃.

 最近几天现场催的比较紧,最近抽空把这个问题处理了一下,在解决问题的过程中我把解决的步骤和方法记录了下来,正好和大家共享一下

现场提的问题概况:

    数据量:生成xml,每个文件100W+条的数据

    内存控制:最好不要超过512M

    问题详情:在处理70W左右的时候内存溢出

一、先来看一下程序要生成的xml文件的结构

Xml代码  

1.     <File>  

2.       <FileType>1</FileType>  

3.       <RType>12</RType>  

4.       <Version>03</Version>  

5.       <BNo>004</BNo>  

6.       <FileQ>5</FileQ>  

7.       <FNo>0006</FNo>  

8.       <RecordNum>1000000</RecordNum>  

9.       <!-- 上面是文件头  下面是百万个<RecordList>  -->  

10.   <RecordList>  

11.     <Msisdn>10350719507</Msisdn>  

12.     <State>1</State>  

13.     <StartDate>20110303</StartDate>  

14.     <Date>20110419</Date>  

15.     <Balance>45000</Balance>  

16.   </RecordList>  

17.    ...  <!-- 可能百万个  <RecordList> -->  

18.  </File>  

 

二、给大家说一下如何把大数据生成xml文件

1、小数据量的情况下    <  1W条数据

       比较好用的方法是使用开源框架,比如XStream直接把javabean 生成 xml

               优点:api操作简单,方便维护

             缺点:数据量大的情况下太消耗内存

 2、大数据量生成一个xml文件(本程序采用的方法)

      自己做的一个可以使用极少的内存生成无限制大的xml文件框架3部分生成xml文件

               第一部分:生成文件头  

                          例如: xxx.toXML(Object obj, String fileName)

               第二部分:通过每次向文件里面追加3000(可配置)条数据的形式生成文件块  

                          例如:xxx.appendXML(Object object);  //object可以是ArrayList 或者一个单独的javaBean

               第三部分:生成xml文件尾巴   

                          例如:xxx.finishXML();

      程序中的调用:调用xxx.toXML(Object obj,String fileName)生成文件头之后,可以循环从数据库中读取数据生成ArrayList,通过xxx.appendXML(Object object)方法追加到xml文件里面,xxx.finishXML()对文件进行收尾

 对框架说明:我上面提供的例子有文件头 +文件块 + 文件尾巴.如果和你们的实际使用文件不太一致的话,可以参考上面提供的思路修改一下即可,主要的方法是把相同的文件块部分分离出来通过追加的形式写入xml文件.

 

有了思路之后,大家可以尝试着自己写一个类似的大数据处理框架(千万级别以上),如何有什么需要帮助的可以直接联系我,因为是公司的程序,不太敢放出来,怕......

三、我是如何测试性能和优化的

 1、手动排除

 根据文件崩溃时候的日志发现是在生成xml的框架里面报的错误,第一想到的是框架有些资源没有释放.于是把自己做的文件生成框架整体的排查了一遍,并且自己写个简单程序生成200万条数据,使用xml框架生成一个xml文件,整个生成过程中任务管理器(xp)查看程序对应的java进程使用的内存基本在20M左右,因此排除框架的问题.怀疑是数据库查询和调用框架的部门出现问题.

检测了一遍主程序的关键部分代码,优化了一下字符串处理.手动的释放一些对象的内存(例如:调用ArrayList.clear(),或者把对象置空等),分配512内存后运行程序,60万数据的时候内存溢出,因为能主动释放的对象都已经释放掉了,还是没有解决,果断放弃看代码,准备使用JProfile进行内存检测.

 2、手动排除没有解决,借助内存分析工具JProfile进行排除

 通过在数据库中生成300W条数据,在JProfile上面多跑程序,一边运行,一边调用JProfile提供的执行GC按钮主动运行垃圾回收,运行50W数据后,通过检测中发现 java.long.String[] oracle.jdbc.driver.Binder[]两个对象的数目一直保持在自增状态,而且数目基本上差不多,对象数目都在200W以上,由于java.long.String[]对象是需要依赖对象而存在的,因此断定问题就出在oracle.jdbc.driver.Binder[]上面,由于改对象存在引用导致String[]不能正常回收.

 3、通过在JProfile对象查看对象的管理

 检测到oracle.jdbc.driver.Binder oracle.jdbc.driver.

T4CPreparedStatement引起,T4CPreparedStatement正好是OraclejdbcOraclePreparedStatement的具体实现,因此断定是在数据库处理方面出现的问题导致oracle.jdbc.driver.Binder对象不能正常释放,通过再一次有目的的检测代码,排查jdbc数据查询的问题,把问题的矛头直至数据库的批处理和事务处理.因此程序是每生成一个文件成功后,会把已经处理的数据转移到对应的历史表中进行备份,而再个表操作的过程中使用了批处理和事务,使用批处理主要是保证执行速度,使用事务主要是保证同时成功和失败。

 4、又因此程序每次从数据库中查询3000条数据处理,所以准备监控oracle.jdbc.driver.Binder的对象数目是否和查询次数对应.,通过在程序中Sysout输出查询次数 + JProfile运行GC测试 Binder,数据匹配,证实是java在数据库批处理的过程中有些问题.

5、专门把批处理代码提取出来通过JProfile内存分析.最终问题定位完毕.

原因如下:100W数据生成一个文件的过程中,等文件生成完毕之后才能把数据库中的数据备份到历史表中,这个时候才能进行事务的提交,也就是执行commit()并且删除原表数据,100W数据按照3000一批写入文件,每批次只是通过PreparedStatement.addBatch();加入到批次里面去,并没有执行PreparedStatement.executeBatch(),而是在commit()之前统一调用的PreparedStatement.executeBatch(),这样的话PreparedStatement就会缓存100W条数据信息,造成了内存溢出.

错误的方法如下:

Java代码  

1.     try{  

2.                 conn.setAutoCommit(false);  

3.                 pst = conn.prepareStatement(insertSql);  

4.                 pstDel = conn.prepareStatement(delSql);  

5.                 pstUpdate = conn.prepareStatement(sql);  

6.                 ...   

7.                 //totalSize = 100W数据 / 3000一批次  

8.                 for (int i = 1; i <= totalSize; i++) {  

9.                       

10.                 client.appendXML(list);  

11.                  

12.             }  

13.             // 错误的使用方法  

14.             client.finishXML();  

15.             pst.executeBatch();  

16.             pstDel.executeBatch();  

17.         }  

18.          ...  

19.         finally {  

20.             try {  

21.                 if (isError) {  

22.                     conn.rollback();  

23.                 }  

24.                 else  

25.                     conn.commit();  

26.                ...  

27.             }  

28.           ...  

29.         }  

正确的方法如下

          try{

Java代码  

1.        conn.setAutoCommit(false);  

2.        pst = conn.prepareStatement(insertSql);  

3.        pstDel = conn.prepareStatement(delSql);  

4.        pstUpdate = conn.prepareStatement(sql);  

5.        ...   

6.        //totalSize = 100W数据 / 3000一批次  

7.        for (int i = 1; i <= totalSize; i++) {  

8.            list = 从数据库中查询3000条数据  

9.            client.appendXML(list);  

10.   

11.       pst.executeBatch();  

12.       pstDel.executeBatch();  

13.    }  

14.    client.finishXML();  

15.      

16.   

17. ...  

18. inally {  

19.    try {  

20.        if (isError) {  

21.            conn.rollback();  

22.        }  

23.        else  

24.            conn.commit();  

25.       ...  

26.    }  

27.  ...  

     如果碰到和我一样的需要给大家一个提醒.oracle在每次执行executeBatch();进行批处理的时候,当前connection对应的rownum会根据操作的结果发生变化.

 在执行pst.executeBatch();之后,当前连接的 rownum数就会发生变化. 因此凡是通过rownum查询数据的程序都要小心这一点

java千万级别数据处理(2)-千万级别FTP下载

这个也是以前做过的一个程序,目的主要是去ftp主机(最多100左右)去取xx数据文件.

     千万级别只是个概念,代表数据量等于千万或者大于千万的数据

       本分享不牵扯分布式采集存储之类的.是在一台机器上处理数据,如果数据量很大很大的话,可以考虑分布式处理,如果以后我有这方面的经验,会及时分享的.

1、程序采用的ftp工具 apache commons-net-ftp-2.0.jar

2、千万级别ftp核心关键的部分--列目录到文件,只要是这块做好了,基本上性能就没有太大的问题了.

  可以通过apache发送ftp命令 "NLST"的方式列目录到文件中去

  # ftp列目录执行的命令以环境变量的配置优先,不配置则使用默认的列目录方式 NLST

Java代码  

1.     # DS_LIST_CMD = NLST   

2.     public File sendCommandAndListToFile(String command,String localPathName) throws   IOException   

3.           {  

4.                   try {  

5.                           return client.createFile(command, localPathName);  

6.                   } catch (IOException e) {  

7.                           log.error(e);  

8.               throw new IOException("the command "+command +" is incorrect");  

9.                   }  

10.       }  

         当然应该还有其他形式的,大家可以自己研究一下

         十万级别以上的数据量的话千万不要使用下面这种方式,如果用的话 ====找死 

            FTPFile[] dirList = client.listFiles();

3、分批次从文件中读取要下载的文件名.  加载到内存中处理,或者读取一个文件名就下载一个文件,不要把所有的数据都加载到内存,如果很多的话会出问题

            为啥要分批次?

            因为是大数据量,如果有1000W条记录,列出来的目录文件的大小 1G以上吧

4、文件下载的核心代码----关于文件的断点续传,获得ftp文件的大小和本地文件的大小进行判断,然后使用ftp提供的断点续传功能

            下载文件一定要使用二进制的形式

           client.enterLocalPassiveMode();//设置为被动模式

            ftpclient.binary(); //一定要使用二进制模式

Java代码  

1.     /** 下载所需的文件并支持断点续传,下载后删除FTP文件,以免重复 

2.              * @param pathName 远程文件 

3.              * @param localPath 本地文件 

4.              * @param registerFileName 记录本文件名称目录 

5.              * @param size 上传文件大小 

6.              * @return true 下载及删除成功 

7.              * @throws IOException  

8.              * @throws Exception 

9.              */  

10.         public boolean downLoad(String pathName, String localPath) throws IOException {  

11.                 boolean flag = false;  

12.                 File file = new File(localPath+".tmp");//设置临时文件  

13.                 FileOutputStream out = null;  

14.                 try{  

15.                     client.enterLocalPassiveMode();// 设置为被动模式  

16.                     client.setFileType(FTP.BINARY_FILE_TYPE);//设置为二进制传输  

17.                         if(lff.getIsFileExists(file)){//判断本地文件是否存在,如果存在并且长度小于FTP文件的长度时断点续传;返之新增  

18.                                 long size = this.getSize(pathName);  

19.                                 long localFileSize = lff.getSize(file);          

20.                                 if(localFileSize > size){  

21.                                         return false;  

22.                                 }  

23.                                 out = new FileOutputStream(file,true);  

24.                                 client.setRestartOffset(localFileSize);  

25.                                 flag = client.retrieveFile(new String(pathName.getBytes(),client.getControlEncoding()),out);  

26.                                   

27.                                 out.flush();  

28.                         } else{  

29.                                 out = new FileOutputStream(file);  

30.                                 flag = client.retrieveFile(new String(pathName.getBytes(),client.getControlEncoding()),out);  

31.                                   

32.                                 out.flush();  

33.                         }  

34.                           

35.                 }catch(IOException e){  

36.                         log.error(e);  

37.             log.error("file download error !");  

38.                         throw e;  

39.                 }finally{  

40.                         try{  

41.                                 if(null!=out)  

42.                                 out.close();  

43.                                 if(flag)  

44.                                         lff.rename(file, localPath);  

45.                         }catch(IOException e){  

46.                                 throw e;  

47.                         }  

48.                 }  

49.                 return flag;  

50.         }  

51.     /** 

52.          * 获取文件长度 

53.          * @param fileNamepath 本机文件 

54.          * @return 

55.          * @throws IOException 

56.          */  

57.         public long getSize(String fileNamepath) throws IOException{  

58.                 FTPFile [] ftp = client.listFiles(new String(fileNamepath.getBytes(),client.getControlEncoding()));  

59.                 return ftp.length==0 ? 0 : ftp[0].getSize();  

60.         }  

61.   

62.         检测本地文件是否已经下载,如果下载文件的大小.  

63.   

64.        /** 

65.          *本地文件的 获取文件的大小 

66.          * @param file 

67.          * @return 

68.          */  

69.         public long getSize(File file){  

70.                 long size = 0;  

71.                 if(getIsFileExists(file)){  

72.                         size = file.length();  

73.                 }  

74.                 return size;  

75.         }  

 

5、因为程序要跑最多100多个线程,在线程监控上做了一些处理,可以检测那些死掉的线程,并及时的拉起来。

            t.setUncaughtExceptionHandler(new ThreadException(exList));

原理:给每个线程添加 UncaughtExceptionHandler,死掉的时候把线程对应的信息加入到一个list里面,然后让主线程每隔一段时间扫描一下list,如果有数据,直接重新建一个线程运行即可

6、如果程序是常驻内存的话,别忘记了在finally中关闭掉不用的ftp连接

7、做大数据库采集程序必须考虑到的一件事情  磁盘空间已满的处理

       java虚拟机对于磁盘空间已满,在英文环境下的  linux aix机器上一般报

       There is not enough space in the file system

       中文环境下一般报 "磁盘空间已满"

       大家可以使用下面的代码进行验证                        

Java代码  

1.     //检测磁盘控件是否已满的异常  

Java代码  

1.     //linux aix There is not enough space in the file system  

2.                          // window There is not enough space in the file system  

3.                          if(e.toString().contains("enough space")||e.toString().contains("磁盘空间已满"))  

4.                          {  

5.                                  log.error("channel "+channel_name + " There is not enough space on the disk ");  

6.                                  Runtime.getRuntime().exit(0);  

7.                          }  

 

 

原创粉丝点击