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

释放双眼,带上耳机,听听看~!

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
正好是
Oracle

jdbcOraclePreparedStatement
的具体实现
,
因此断定是在数据库处理方面出现的问题导致
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.     
                     }  

 

 

给TA打赏
共{{data.count}}人
人已打赏
安全技术

bootstrap栅格系统自定义列

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索