MySQL数据库流式查询的使用

MySQL数据库流式查询的使用

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条数据并打印,普通查询与流式查询耗时情况与内存变化图如下:

  • 普通查询
    MySQL数据库流式查询的使用_第1张图片
    MySQL数据库流式查询的使用_第2张图片
  • 流式查询
    MySQL数据库流式查询的使用_第3张图片
    MySQL数据库流式查询的使用_第4张图片
    由图对比,可发现流式查询相较于普通查询而言:耗时稍少;且占用内存区域平稳,不会导致OOM产生。

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并安装,如下图:
MySQL数据库流式查询的使用_第5张图片
使用时选择对应的橘黄色Run和Debug按钮,会自动弹出VisualVM的视图界面(注:首次点击使用会弹出VisualVM应用程序绑定框,直接选择本地相应路径保存即可,如:D:\jdk\jdk1.8.72_X64\bin\jvisualvm.exe):
MySQL数据库流式查询的使用_第6张图片
2.2 Spring Data Jpa中的使用
部分主要代码片段如下:

  • POJO实体类(Column注解引自javax.persistence.Column,为了避免实体类与数据库表字段名称不不一致,在实体类字段上注解注明表字段名称,后面反射对应):
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;
    }

}
  • 自定义JdbcTemplate继承org.springframework.jdbc.core.JdbcTemplate,最终调用其带回调函数的query方法(即RowCallbackHandler接口),在实现该接口时,调用我们自定义的回调函数,并实现自己的业务处理逻辑
@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));
    }

}
  • 测试结果如下:
    1)普通查询:
    MySQL数据库流式查询的使用_第7张图片
    MySQL数据库流式查询的使用_第8张图片
    2)流式查询
    MySQL数据库流式查询的使用_第9张图片
    MySQL数据库流式查询的使用_第10张图片
    由图对比可知,流式查询相较于普通查询而言:耗时较少;且整体内存占用少(普通:最大接近1.1G,流式:最大接近650M),趋于平稳,不断释放,不会造成OOM。

2.3 mybatis中的使用

mybatis中可直接在配置文件中配置,相对比较简单,后续可补充。

3. 后记

不合理或错误之处还请指出,谢谢。

你可能感兴趣的:(MySQL)