背景
java 导出数据库数据,在数据量比较大的情况下,如果全部将数据读到内存中再执行写文件,很容易遇到内存溢出;为了避免内存溢出的问题,使用游标的方式,边读边写;
分页的方式,当数据量较大的情况下,需要花费一些建立数据库连接的消耗,也不是很推荐,所以可以尝试使用游标的方式;
数据准备
BEGIN
FOR i IN 1..2000000
LOOP
-- 插入数据SQL
INSERT INTO ......
--
IF
i MOD 10000 = 0 THEN
COMMIT;
END IF;
END LOOP;
END;
通过循环的方式插入200W数据,为了提高插入效率,1万条提交一次;
导出数据方法
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.io.*;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
@Slf4j
public class DataExportTest {
public static final String URL = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
public static final String DRIVER = "oracle.jdbc.OracleDriver";
public static final String PASSWORD = "youdasi";
public static final String USERNAME = "youdasi";
@Test
public void exportTest() throws Exception {
//初始化数据库连接
Connection connection = initConnection();
Statement statement = connection.createStatement();
//FETCH_SIZE 的大小视具体情况而定
statement.setFetchSize(1000);
ResultSet resultSet = statement.executeQuery("SELECT * FROM TEST");
ResultSetMetaData metaData = resultSet.getMetaData();
long begin = System.currentTimeMillis();
//数据库每行数据
StringBuilder rowData = new StringBuilder();
//单次写入文件的数据(可以删掉)
List<String> pageData = new LinkedList<String>();
//result 为数据库
int j = 0;
int num = metaData.getColumnCount();
while (resultSet.next()){
j++;
//组装单行数据
for (int i = 1; i< num ; i++){
Object object = resultSet.getObject(i);
if (null != object){
rowData.append(object.toString());
}
rowData.append("|");
}
pageData.add(rowData.toString());//添加到写文件集合
rowData.setLength(0);//清空行数据
//10W条数据写一次,视情况而定
if (j % 100000 == 0){
log.info(j + "");
writeFile(pageData,"C:/temp/dat/test.dat");//写数据
pageData.clear(); //清空list
}
}
if (j % 100000 != 0){
writeFile(pageData,"C:/temp/dat/test.dat");//写数据
}
long end = System.currentTimeMillis();
log.info("time: {}",end - begin);
}
/**
* 初始化数据库连接
*/
private Connection initConnection(){
Connection connection = null;
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (ClassNotFoundException e) {
log.error("error load jdbc driver");
} catch (SQLException e) {
log.error("error get connection");
}
return connection;
}
/**
* 写数据到文件
*/
private void writeFile(List<String> data,String path) throws IOException {
if (null == data || data.size() == 0){
return;
}
File file = new File(path);
if (!file.exists()){
file.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(path, true);
OutputStreamWriter write = new OutputStreamWriter(fileOutputStream, "UTF-8");
BufferedWriter writer = new BufferedWriter(write);
for (String str:data){
writer.write(str);
writer.flush();
}
writer.close();
write.close();
fileOutputStream.close();
}
}
代码中,有下面几个地方可以思考下:
- FETCH_SIZE 取多大合适,我本地虚拟机docker起的ORACLE数据库,测试出1000比较合适,但是还是需要自己测试选择合适的;
- PAGE_SIZE 取多少合适,也是需要视情况而定
- 可以看出代码中有List pageData,这个其实可以直接省略掉,只用一个String,这样做其实只是让逻辑看起来清楚些,但是损耗了性能
- 写文件的方法,每次新建了流再关闭,其实可以使用一个流,每次flush,最后一次再关闭就可以了,这样也可以提升部分效率
- 代码中存在字符串的拼接,这个其实可以放到SQL中来做,这样就把字符拼接的工作从程序转移到数据库,至于哪个更好,不确定
SELECT '字段名'||'字段名' AS RES_STR FROM '表名' WHERE ...;
- 游标读取数据的时候,有用map封装的情况,改成数组效率会高很多,比如
List<Map<Integer, String>> bufferData = new LinkedList<>();
int rowNum = 0;
while (resultSet.next()) {
rowNum++;
Map<Integer, String> rowData = new HashMap<>(num);
for (int i = 1; i < num; i++) {
rowData.put(i, resultSet.getString(i));
}
bufferData.add(rowData);
}
改为
List<String[]> bufferData = new LinkedList<>();
int rowNum = 0;
while (resultSet.next()) {
String[] rowData = new String[num];
rowNum++;
for (int i = 1; i < num; i++) {
rowData[i] = resultSet.getString(i);
}
bufferData.add(rowData);
}
最终运行结果
- 导出纪录数:200W
- 导出文件大小:544 MB
- 耗时 :29079ms=29s