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.
}