目标
最近公司要求做一个web版ETL工具,需要一次性查询出来任意一张表的所有的数据,有一张表到达了1亿两千万,所有经过探索采用了以下的方案
先说一下内存溢出的原因
jdbc查询mysql时,默认会一次性将sql查询的数据全部从服务器加载到内存中,当数据过多时,导致内存溢出
解决方式
1.设置每次从服务器加载到内存的条数
statement.setFetchSize(batchSize);
参考资料 jdbc官网:https://docs.oracle.com/database/121/JJDBC/resltset.htm#JJDBC28620 2.设置ResultSet对象的大小的限制
statement.setMaxRows(int i)
参考solr
参考solr源码时,发现solr为什么查询一亿多数据的时候,不会oom,平常8g内存 1千万数据就会oom了,
原来 solr 重写了一个Iterator
并且把jdbc 设置了每次读取数据库并不读取全部,而且读取一部分流
jdbc 的设置
Connection创建 Statement时,更改默认的配置
原先默认配置 的方法都能够实现任意的前后滚动,使用各种移动的ResultSet指针的方法,不能够释放内存,导致查询数据过多时,oom,所以修改为
CONCUR_READ_ONLY,参数只允许结果集的游标向下移动,移动的同时也就释放掉了内存
Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
statement.setFetchSize(batchSize); // 设置从数据库取得多行的行数大小
statement.setMaxRows(maxRows); // 将此 Statement 对象生成的所有 ResultSet 对象可以包含的最大行数限制设置为给定数
解决方案(模仿solr 编写查询迭代器)
完整代码git版 https://gitee.com/hugo110/common-utils/blob/master/src/main/java/com/fanfan/demo/solrjdbc/JdbcDataSource.java JdbcDataSource和ResultSetIterator
package com.fanfan.demo.solrjdbc;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.fanfan.demo.solrjdbc.model.DataBaseModel;
import com.fanfan.utils.database.ConnectionUtil;
import com.fanfan.utils.database.JdbcUtil;
import lombok.Data;
/**
* 封装了 根据sql 封装的链接池
*
* @ClassName JdbcDataSource
* @author <a href="892042158@qq.com" target="_blank">于国帅</a>
* @date 2019年2月21日 下午3:01:36
*
*/
//@Slf4j
public class JdbcDataSource {
private DataBaseModel databaseModel; // 链接数据的信息
public JdbcDataSource(DataBaseModel databaseModel) {
this.databaseModel = databaseModel;
}
public static void main(String[] args) {
DataBaseModel jdbcModel = new DataBaseModel();
jdbcModel.setDbDriver("com.mysql.jdbc.Driver");
jdbcModel.setDbUrl(
"jdbc:mysql://127.0.0.1:3306/springboot-boot?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false");
jdbcModel.setDbUserName("root");
jdbcModel.setDbPassword("root");
JdbcDataSource jdbcDataSource = new JdbcDataSource(jdbcModel);
String sql = "select * from t_system_menu";
Iterator<Map<String, Object>> iterator = jdbcDataSource.getResultSetIterator(sql);
int i = 0;
while (iterator.hasNext()) {
System.err.println(iterator.next());
i++;
}
System.err.println("总共+" + i);
}
// 支持查询
public Iterator<Map<String, Object>> getResultSetIterator(String querySql) {
try {
// 返回迭代器 // 初始化 Statement // 执行查询
return new ResultSetIterator(getConnection(), querySql);
} catch (SQLException e) {
return null;
}
}
protected Connection getConnection() {
// 初始化数据源
ConnectionUtil.init(databaseModel.getDbDriver(), databaseModel.getDbUrl(), databaseModel.getDbUserName(),
databaseModel.getDbPassword());
// 初始化连接 Connection
Connection connection = ConnectionUtil.createConnection(); // 返回对应的链接
// 如果使用连接池 ,那么把链接放到连接池里面等待使用 选择关闭就可以
return connection;
}
}
/**
* 封装大数据 Iterator 能够实现对应的数据源获取
*
* @获取的model Map<String, Object> key 列名称,value 列数据
* @ClassName ResultSetIterator
* @author <a href="892042158@qq.com" target="_blank">于国帅</a>
* @date 2019年2月21日 下午3:02:48
*
*/
@Data
class ResultSetIterator implements Iterator<Map<String, Object>> {
private ResultSet resultSet; // 查询出来的结果集
private List<String> fieldNameList; // 需要查询的字段的集合
public ResultSetIterator(Connection connection, String querySql) throws SQLException {
// 初始化 Statement 防止又一次查询把之前的结果集关闭
Statement statement = JdbcUtil.createStatement(connection, 500, 0);
// 执行查询
this.resultSet = JdbcUtil.executeStatement(statement, querySql);
// 设置字段
this.fieldNameList = JdbcUtil.getFieldNameList(this.resultSet.getMetaData());
}
@Override
public boolean hasNext() {
try {
return resultSet.next();
} catch (SQLException e) {
// 关闭链接
return false;
}
}
/**
* 返回当前结果集的一条
*
* @Title next
* @author 于国帅
* @date 2019年2月21日 下午4:09:05
* @return
* @see java.util.Iterator#next()
*/
@Override
public Map<String, Object> next() {
Map<String, Object> rowMap = new HashMap<>();
try {
for (String fieldName : this.fieldNameList) {
// Use underlying database's type information except for BigDecimal and BigInteger
// which cannot be serialized by JavaBin/XML. See SOLR-6165
Object value;
value = this.resultSet.getObject(fieldName);
if (value instanceof BigDecimal || value instanceof BigInteger) {
rowMap.put(fieldName, value.toString());
} else {
rowMap.put(fieldName, value);
}
}
} catch (SQLException e) {
return rowMap;
}
return rowMap;
}
}
DataBaseModel
package com.fanfan.demo.solrjdbc.model;
import lombok.Data;
/**
* 链接数据库的model 属性
*
* @ClassName DataBaseModel
* @author <a href="892042158@qq.com" target="_blank">于国帅</a>
* @date 2019年2月21日 下午2:56:37
*
*/
@Data
public class DataBaseModel {
// id
private Long id;
// 数据库的名称
private String dbName;
// 数据库的类型 ,例如是mysql 还是oracle
private String type;
// 链接数据库的URL
private String dbUrl;
// 使用的驱动名称
private String dbDriver;
// 用户名称
private String dbUserName;
// 密码
private String dbPassword;
}
工具类
package com.fanfan.utils.database;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* 操作大数据 jdbc 工具类
*
* @ClassName JdbcUtil
* @author <a href="892042158@qq.com" target="_blank">于国帅</a>
* @date 2019年2月21日 下午4:37:47
*
*/
public class JdbcUtil {
/**
* 获取当前结果集的所有字段名称
*
* @Title getFieldNameList
* @author 于国帅
* @date 2019年2月21日 下午4:14:18
* @param metaData
* @return
* @throws SQLException
* List<String>
*/
public static List<String> getFieldNameList(ResultSetMetaData metaData) {
List<String> colNameList = new ArrayList<>();
try {
int count = metaData.getColumnCount() + 1;
for (int i = 1; i < count; i++) {
colNameList.add(metaData.getColumnLabel(i));
}
} catch (SQLException e) {
return colNameList;
}
return colNameList;
}
/**
* 获取 只能够向下移动的 Statement
*
* @Title createStatement
* @author 于国帅
* @date 2019年2月21日 下午4:38:07
* @param connection
* @param batchSize
* @param maxRows
* @return
* @throws SQLException
* Statement
*/
public static Statement createStatement(final Connection connection, final int batchSize, final int maxRows) throws SQLException {
// TYPE_FORWARD_ONLY 而默认的TYPE_FORWARD_ONLY参数只允许结果集的游标向下移动。 在从ResultSet(结果集)中读取记录的时,对于访问过的记录就自动释放了内存。
// ResultSet.CONCUR_READ_ONLY 就是类似只读 属性,不可仪更改的啊!不能用结果集更新数据。
Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
statement.setFetchSize(batchSize); // 设置从数据库取得多行的行数大小
statement.setMaxRows(maxRows); // 将此 Statement 对象生成的所有 ResultSet 对象可以包含的最大行数限制设置为给定数
return statement;
}
public static ResultSet executeStatement(Statement statement, String query) throws SQLException {
boolean resultSetAvailable = statement.execute(query); // 如果是查询的话返回true,如果是更新或插入的话就返回false了;
while (!resultSetAvailable && statement.getUpdateCount() != -1) {
resultSetAvailable = statement.getMoreResults(); // Statement提供了一个getMoreResults()的方法,该方法能将当前Statement "指针" 移动到下一个结果集.
}
if (resultSetAvailable) {
return statement.getResultSet();
}
return null;
}
}
ConnectionUtil
package com.fanfan.utils.database;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import com.mysql.jdbc.exceptions.jdbc4.CommunicationsException;
/**
* 数据库连接工具类 主要是创建连接
*
* @author JueYue
* @date 2014年12月21日
*/
public class ConnectionUtil {
private static Connection connection;
private static String DB_DRIVER;
private static String DB_URL;
private static String DB_USER_NAME;
private static String DB_PASSWORD;
private static ThreadLocal<Connection> connectionTl = new ThreadLocal<>();
private ConnectionUtil() {
}
private static ConnectionUtil instance;
public static Statement createStatement() {
try {
/* DatabaseMetaData dbMeta = connectionTl.get().getMetaData();
//System.out.println("1111111111===="+dbMeta.supportsColumnAliasing()); */
return connectionTl.get().createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
} catch (Exception e) {
throw new RuntimeException("创建 Statement 发生异常", e);
}
}
public static Connection createConnection() {
try {
return connectionTl.get();
} catch (Exception e) {
throw new RuntimeException("创建 Statement 发生异常", e);
}
}
public static Statement createThisStatement() {
if (instance == null) {
initThis();
}
try {
return connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
} catch (Exception e) {
throw new RuntimeException("创建 Statement 发生异常", e);
}
}
private static void initThis() {
try {
Class.forName(DB_DRIVER);
connection = DriverManager.getConnection(DB_URL, DB_USER_NAME, DB_PASSWORD);
} catch (Exception e) {
throw new RuntimeException("创建 Connection 发生异常", e);
}
}
public static void init(String deiver, String url, String username, String passwd) {
try {
// System.out.println(System.currentTimeMillis());
Class.forName(deiver);
// System.out.println(System.currentTimeMillis());
connectionTl.set(DriverManager.getConnection(url, username, passwd));
// System.out.println(System.currentTimeMillis());
} catch (Exception e) {
throw new RuntimeException("创建 Connection 发生异常", e);
}
}
public static void checkConnection(String deiver, String url, String username, String passwd) {
try {
Driver driver = (Driver) Class.forName(deiver).newInstance();
boolean urlflag = driver.acceptsURL(url);
if (!urlflag) {
throw new RuntimeException("请检查数据库地址!");
}
DriverManager.getConnection(url, username, passwd);
} catch (ClassNotFoundException e) {
throw new RuntimeException("请检查数据库类型!", e);
} catch (CommunicationsException e) {
throw new RuntimeException("请检查数据库地址!", e);
} catch (SQLException e) {
throw new RuntimeException("请检查用户名或密码,是否正确!", e);
} catch (Exception e) {
throw new RuntimeException("请检查驱动类型与数据库地址是否对应!", e);
}
}
public static void close() {
try {
if (!connectionTl.get().isClosed())
connectionTl.get().close();
} catch (Exception e) {
throw new RuntimeException("关闭 Connection 发生异常", e);
}
}
public static void closeThis() {
try {
if (!connection.isClosed())
connection.close();
} catch (Exception e) {
throw new RuntimeException("关闭 Connection 发生异常", e);
}
}
public static void setDB_DRIVER(String dB_DRIVER) {
DB_DRIVER = dB_DRIVER;
}
public static void setDB_URL(String dB_URL) {
DB_URL = dB_URL;
}
public static void setDB_USER_NAME(String dB_USER_NAME) {
DB_USER_NAME = dB_USER_NAME;
}
public static void setDB_PASSWORD(String dB_PASSWORD) {
DB_PASSWORD = dB_PASSWORD;
}
}
知识参考
1.Connection
createStatement(int resultSetType, int resultSetConcurrency)
默认这两个参数的共同特点是允许结果集(ResultSet)的游标可以上下移动
加载大量数据后,访问过内存不会释放,过多导致OOM
2.Statement
setFetchSize
设置从数据库取得多行的行数大小
setMaxRows
将此 Statement 对象生成的所有 ResultSet 对象可以包含的最大行数限制设置为给定数
setFetchDirection
向驱动程序提供关于方向的提示,在使用此 Statement 对象创建的 ResultSet 对象中将按该方向处理行。可以设置的参数是ResultSet.FETCH_FORWARD 、ResultSet.FETCH_REVERSE 和 ResultSet.FETCH_UNKNOWN。
getUpdateCount
可以知道成功更新了多少记录,如果返回值为0,就说明没有1条记录被更新
getMoreResults
Statement提供了一个getMoreResults()的方法,该方法能将当前Statement “指针” 移动到下一个结果集.
execute
execute 方法返回一个 boolean 值,以指示第一个结果的形式。必须调用 getResultSet 或 getUpdateCount 方法来检索结果,并且必须调用 getMoreResults 移动到任何后面的结果。
如果第一个结果是 ResultSet 对象,则返回 true;如果第一个结果是更新计数或者没有结果,则返回 false
意思就是如果是查询的话返回true,如果是更新或插入的话就返回false了;
3.ResultSet
3.1 变量
int FETCH_FORWARD = 1000;
int FETCH_REVERSE = 1001;
int FETCH_UNKNOWN = 1002;
int TYPE_FORWARD_ONLY = 1003;(在从ResultSet(结果集)中读取记录的时,对于访问过的记录就自动释放了内存。
只能向前滚动(这是默认值))
int TYPE_SCROLL_INSENSITIVE = 1004;(这两个方法都能够实现任意的前后滚动,使用各种移动的ResultSet指针的方法。二者的区别在于前者对于修改不敏感,而后者对于修改敏感。)
int TYPE_SCROLL_SENSITIVE = 1005;(这两个方法都能够实现任意的前后滚动,使用各种移动的ResultSet指针的方法。二者的区别在于前者对于修改不敏感,而后者对于修改敏感。)
int CONCUR_READ_ONLY = 1007;(就是类似只读 属性,不可仪更改的啊!不能用结果集更新数据。只允许结果集的游标向下移动。)
int CONCUR_UPDATABLE = 1008;(ResultSet对象可以执行数据库的新增、修改、和移除。)
int HOLD_CURSORS_OVER_COMMIT = 1;
int CLOSE_CURSORS_AT_COMMIT = 2;
方法
getMetaData
结果集对象ResultSet中提供的一些信息