1. 背景
MySQL使用流式查询的目的,简单来说,就是避免查询数据量过大导致OOM。
详情可参考:https://www.jianshu.com/p/eedc9350f700
本文主要介绍流式查询在JDBC、JPA中的使用方法。
2. 使用
2.1 JDBC中的使用
部分主要代码片段如下:
@Test
public void testQuery() {
long startTime = System.currentTimeMillis();
//查询并处理逻辑
String sql = "select * from ordersummary limit 300000";
execute(sql, false);
long endTime = System.currentTimeMillis();
System.out.println("总记录条数为:" + count);
System.out.println("消耗时间为:" + (endTime - startTime));
}
private void execute(String sql, boolean isStreamQuery) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
//获取数据库连接
conn = getConnection();
if (isStreamQuery) {
//设置流式查询参数
stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);
} else {
//普通查询
stmt = conn.prepareStatement(sql);
}
//执行查询获取结果
rs = stmt.executeQuery();
//遍历结果
while(rs.next()){
System.out.println(
rs.getString(1) + "=" + rs.getString(2) + "="
+ rs.getString(3) + "=" + rs.getString(4) + "="
+ rs.getString(5) + "=" + rs.getString(6) + "="
+ rs.getString(7) + "=" + rs.getString(8) + "="
+ rs.getString(9) + "=" + rs.getString(10)
);
count++;
}
} catch (SQLException e) {
//handle exception
} finally {
close(stmt, rs, conn);
}
}
查询30W条数据并打印,普通查询与流式查询耗时情况与内存变化图如下:
PS:内存查看工具,我这里使用的是jdk的VisualVM工具(题外话,可跳过)。使用方法如下:
1)可直接在jdk安装目录bin下(eg:D:\jdk\jdk1.8.72_X64\bin),找到jvisualvm.exe,打开即可;然后在程序运行时,列表中选择正在运行的程序名称,即可查看内存变化图;同时,也可按需查看CPU、类及线程的数据。
2)IntelliJ IDEA的插件中(File->Setting->Plugins)搜索VisualVM Launcher并安装,如下图:
使用时选择对应的橘黄色Run和Debug按钮,会自动弹出VisualVM的视图界面(注:首次点击使用会弹出VisualVM应用程序绑定框,直接选择本地相应路径保存即可,如:D:\jdk\jdk1.8.72_X64\bin\jvisualvm.exe):
2.2 Spring Data Jpa中的使用
部分主要代码片段如下:
public class OrderSummary {
@Column(name = "payorderid")
private String payorderid;
@Column(name = "mercharset")
private String mercharset;
@Column(name = "merchantid")
private String merchantid;
@Column(name = "amount")
private BigDecimal amount;
@Column(name = "subamount")
private BigDecimal subamount;
@Column(name = "payamount")
private BigDecimal payamount;
......
}
public interface ResultHandler {
void handleResult(ResultContext context);
}
public interface ResultContext {
T getResultObject();
}
public class DefaultResultContext implements ResultContext {
private T resultObject;
public DefaultResultContext() {
this.resultObject = null;
}
public void nextResultObject(T resultObject) {
this.resultObject = resultObject;
}
@Override
public T getResultObject() {
return resultObject;
}
}
@Component
public class JdbcTemplate extends org.springframework.jdbc.core.JdbcTemplate {
public void streamQuery(String sql, Class cls, ResultHandler handler) {
this.streamQuery(sql, cls, handler, null);
}
public void streamQuery(final String sql, final Class cls, final ResultHandler handler, final Object...params) {
this.query((connection) -> {
PreparedStatement stmt = null;
try {
stmt = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);
if (null != params) {
//设置sql参数
for (int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
}
} catch (SQLException e) {
//handle exception
}
return stmt;
}, (resultSet) -> {
//映射PO,装载到ResultContext对象中
ResultContext resultContext = null;
try {
resultContext = dealResult(resultSet, cls);
} catch (IllegalAccessException e) {
//handle exception
} catch (InstantiationException e) {
//handle exception
} catch (InvocationTargetException e) {
//handle exception
}
//回调handler中的方法 执行业务逻辑
handler.handleResult(resultContext);
});
}
/**
* 处理查询结果,封装到指定PO对象中 .
*
* @param rs
* @param cls
* @return
* @throws SQLException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws InvocationTargetException
*/
private ResultContext dealResult(ResultSet rs, Class cls) throws SQLException, IllegalAccessException, InstantiationException, InvocationTargetException {
//key:column name value:field name
Map fieldColumnMap = getFieldColumnMap(cls);
Object obj = cls.newInstance();
DefaultResultContext defaultResultContext = new DefaultResultContext();
//获取映射PO类的所有方法对象
List methods = Arrays.asList(cls.getDeclaredMethods());
for (Map.Entry entry : fieldColumnMap.entrySet()) {
//数据库表列名
String columnName = entry.getKey();
Object columnValue = rs.getObject(columnName);
//PO类字段名
String fieldName = entry.getValue();
//为数据表列名所对应的字段进行值的设置
for (Method method : methods) {
String methodName = method.getName();
if (methodName.startsWith("set") && (methodName.length() > 3)) {
//切割方法名前面的"set"
String subMethodName = methodName.substring(3);
//匹配字段名
if (subMethodName.equalsIgnoreCase(fieldName)) {
method.invoke(obj, columnValue);
}
}
}
}
//PO类对象封装完成
defaultResultContext.nextResultObject(obj);
return defaultResultContext;
}
/**
* 获取PO类对象中列名与属性名的对应关系map .
*
* @param clazz PO类字节码文件
* @return
*/
private Map getFieldColumnMap(Class> clazz) {
//定义一个map,存储字段与数据表列名的对应关系
Map fieldColumnMap = new HashMap();
//获取映射PO类中的所有字段对象
List fields = Arrays.asList(clazz.getDeclaredFields());
if (!CollectionUtils.isEmpty(fields)) {
for (Field field : fields) {
String columnName = "";
String filedName = field.getName();
//是否使用Column注解
if (field.isAnnotationPresent(Column.class)) {
//获取自己所有的注解
Annotation[] annotations = field.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
//找到自己的Column注解
if (annotation.annotationType().equals(Column.class)) {
//获取注解的值
Column column = field.getAnnotation(Column.class);
columnName = column.name();
if (StringUtils.isEmpty(columnName)) {
continue;
}
fieldColumnMap.put(columnName, filedName);
}
}
}
}
}
return fieldColumnMap;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class StreamQueryTemplateTest {
@Autowired
JdbcTemplate jdbcTemplate;
private int count = 0;
@Test
public void testQuery() {
long startTime = System.currentTimeMillis();
String sql = "select * from ordersummary limit 300000";
jdbcTemplate.streamQuery(sql, OrderSummary.class, (ResultHandler) (context) -> {
OrderSummary order = context.getResultObject();
//TODO handle results
System.out.println(order);
count ++;
});
long endTime = System.currentTimeMillis();
System.out.println("总记录条数为:" + count);
System.out.println("消耗时间为:" + (endTime - startTime));
}
}
2.3 mybatis中的使用
mybatis中可直接在配置文件中配置,相对比较简单,后续可补充。
3. 后记
不合理或错误之处还请指出,谢谢。