springboot-mybatis/JPA流式查询的多种实现方式
作者:Fire_Bit 发布时间:2021-07-07 17:25:51
项目中有几个batch需要检查所有的用户参与的活动的状态,以前是使用分页,一页一页的查出来到内存再处理,但是随着数据量的增加,效率越来越低。于是经过一顿搜索,了解到流式查询这么个东西,不了解不知道,这一上手,爱的不要不要的,效率贼高。项目是springboot 项目,持久层用的mybatis,整好mybatis的版本后,又研究了一下JPA的版本,做事做全套,最后又整了原始的JDBCTemplate 版本。废话不多说,代码如下:
第一种方式: springboot + mybatis 流式查询(网上说的有三种,我觉得下面这种最简单,对业务代码侵入性最小)
a) service 层代码:
package com.example.demo.service;
import com.example.demo.bean.CustomerInfo;
import com.example.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Slf4j
@Service
public class TestStreamQueryService {
@Resource
private ApplicationContext applicationContext;
@Resource
private UserMapper userMapper;
@Resource
private JdbcTemplate jdbcTemplate;
@Transactional
public void testStreamQuery(Integer status) {
mybatisStreamQuery(status);
}
private void mybatisStreamQuery(Integer status) {
log.info("waiting for query.....");
Cursor<CustomerInfo> customerInfos = userMapper.getCustomerInfo(status);
log.info("finish query!");
for (CustomerInfo customerInfo : customerInfos) {
//处理业务逻辑
log.info("===============>{}", customerInfo.getId());
}
}
}
需要注意的有两点:
1.是userMapper 返回的是一个Cursor类型,其实就是用游标。然后遍历这个cursor,mybatis就会按照你在userMapper里设置的fetchSize 大小,每次去从数据库拉取数据
2.注意 testStreamQuery 方法上的 @transactional 注解,这个注解是用来开启一个事务,保持一个长连接(就是为了保持长连接采用的这个注解),因为是流式查询,每次从数据库拉取固定条数的数据,所以直到数据全部拉取完之前必须要保持连接状态。(顺便提一下,如果说不想让在这个testStreamQuery 方法内处理每条数据所作的更新或查询动作都在这个大事务内,那么可以另起一个方法 使用required_new 的事务传播,使用单独的事务去处理,使事务粒度最小化。如下图:)
b) mapper 层代码:
package com.example.demo.mapper;
import com.example.demo.bean.CustomerInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper {
Cursor<CustomerInfo> getCustomerInfo(Integer status);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="getCustomerInfo" resultType="com.example.demo.bean.CustomerInfo" fetchSize="2" resultSetType="FORWARD_ONLY">
select * from table_name where status = #{status} order by id
</select>
</mapper>
UserMapper.java 无需多说,其实要注意的是mapper.xml中的配置:fetchSize 属性就是上一步说的,每次从数据库取多少条数据回内存。resultSetType属性需要设置为 FORWARD_ONLY, 意味着,查询只会单向向前读取数据,当然这个属性还有其他两个值,这里就不展开了。
至此,springboot+mybatis 流式查询就可以用起来了,以下是执行结果截图:
c)读取200万条数据,每次fetchSize读取1000条,batch总用时50s左右执行完,速度是相当可以了,堆内存占用不超过250M,这里用的数据库是本地docker起的一个postgre, 远程数据库的话,耗时可能就不太一样了
第二种方式:springboot+JPA 流式查询
a) service层代码:
package com.example.demo.service;
import com.example.demo.dao.CustomerInfoDao;
import com.example.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import java.util.stream.Stream;
@Slf4j
@Service
public class TestStreamQueryService {
@Resource
private ApplicationContext applicationContext;
@Resource
private UserMapper userMapper;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private CustomerInfoDao customerInfoDao;
@Resource
private EntityManager entityManager;
@Transactional(readOnly = true)
public void testStreamQuery(Integer status) {
jpaStreamQuery(status);
}
public void jpaStreamQuery(Integer status) {
Stream<com.example.demo.entity.CustomerInfo> stream = customerInfoDao.findByStatus(status);
stream.forEach(customerInfo -> {
entityManager.detach(customerInfo); //解除强引用,避免数据量过大时,强引用一直得不到GC 慢慢会OOM
log.info("====>id:[{}]", customerInfo.getId());
});
}
}
注意点:1. 这里的@transactional(readonly=true) 这里的作用也是保持一个长连接的作用,同时标注这个事务是只读的。
2. 循环处理数据时需要先:entityManager.detach(customerInfo); 解除强引用,避免数据量过大时,强引用一直得不到GC 慢慢会OOM。
b) dao层代码:
package com.example.demo.dao;
import com.example.demo.entity.CustomerInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.stereotype.Repository;
import javax.persistence.QueryHint;
import java.util.stream.Stream;
import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE;
@Repository
public interface CustomerInfoDao extends JpaRepository<CustomerInfo, Long> {
@QueryHints(value=@QueryHint(name = HINT_FETCH_SIZE,value = "1000"))
Stream<CustomerInfo> findByStatus(Integer status);
}
注意点:1.dao方法的返回值是 Stream 类型
2.dao方法的注解:@QueryHints(value=@QueryHint(name = HINT_FETCH_SIZE,value = "1000")) 这个注解是设置每次从数据库拉取多少条数据,自己可以视情况而定,不可太大,反而得不偿失,一次读取太多数据数据库也是很耗时间的。。。
自此springboot + jpa 流式查询代码就贴完了,可以happy了,下面是执行结果:
c) batch读取两百万条数据,堆内存使用截图:
每次fetchSize拉取1000条数据,可以看到内存使用情况:初始内存不到100M,batch执行过程中最高内存占用300M出头然后被GC。读取效率:不到一分钟执行完(处理每一条数据只是打印一下id),速度还是非常快的。
d) 读取每一条数据时,不使用 entityManager.detach(customerInfo),内存使用截图:
最终OOM了,这里的entityManager.detach(customerInfo) 很关键。
第三种方式:使用JDBC template 流式查询
其实这种方式就是最原始的jdbc的方式,代码侵入性很大,逼不得已也不会使用
a) 上代码:
package com.example.demo.service;
import com.example.demo.dao.CustomerInfoDao;
import com.example.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Slf4j
@Service
public class TestStreamQueryService {
@Resource
private ApplicationContext applicationContext;
@Resource
private UserMapper userMapper;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private CustomerInfoDao customerInfoDao;
@Resource
private EntityManager entityManager;
public void testStreamQuery(Integer status) {
jdbcStreamQuery(status);
}
private void jdbcStreamQuery(Integer status) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = jdbcTemplate.getDataSource().getConnection();
conn.setAutoCommit(false);
pstmt = conn.prepareStatement("select * from customer_info where status = " + status + " order by id", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(1000);
pstmt.setFetchDirection(ResultSet.FETCH_FORWARD);
rs = pstmt.executeQuery();
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
String email = rs.getString("email");
int sta = rs.getInt("status");
log.info("=========>id:[{}]", id);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
rs.close();
pstmt.close();
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
b) 执行结果:200万数据不到50秒执行完,内存占用最高300M
自此,针对不同的持久层框架, 使用不同的流式查询,其实本质是一样的,归根结底还是驱动jdbc做事情。以上纯个人见解,若有不当之处,请不吝指出,共同进步!
来源:https://blog.csdn.net/new__person/article/details/128364759


猜你喜欢
- Looper是什么用于为线程运行消息循环的类。默认情况下,线程没有与之关联的消息循环。要创建一个,在要运行循环的线程中调用 prepare(
- 一、项目简述本系统功能包括: 系统管理,招生计划,学生管理,录取结果,自动分配,调剂管理等等。二、项目运行环境配置:Jdk1.8 + Tom
- 这是 Java 网络爬虫系列博文的第二篇,在上一篇 Java 网络爬虫新手入门详解 中,我们简单的学习了一下如何利用 Java 进行网络爬虫
- 本案例通过使用JFileChooser实现对选定文件夹内图片实现自动播放和暂停播放代码如下,如有不合适的地方 还请指教package com
- mybatis-plus Condition拼接Sql语句各方法1.setSqlSelect—用于添加查询的列信息public Wrappe
- //去title requestWindowFeature(Window.FEATURE_NO_TITLE); //隐藏状态栏 getWin
- FileWriter/FileReader介绍:FileWriter 类从 OutputStreamWriter 类继承而来。该类按字符向流
- 前言大富翁,又名地产大亨。是一种多人策略图版游戏。参与者分得游戏金钱,凭运气(掷骰子)及交易策略,买地、建楼以赚取租金。英文原名monopo
- 本文实例讲述了Android开发判断一个app应用是否在运行的方法。分享给大家供大家参考,具体如下:在一个应用中,或一个Service 、R
- 一、引入maven依赖Spring Boot默认使用LogBack,但是我们没有看到显示依赖的jar包,其实是因为所在的jar包spring
- 本文实例分析了Android中ListActivity用法。分享给大家供大家参考,具体如下:程序如下:import android.app.
- Java对象内存构成今天来讲些抽象的东西 -- 对象头,因为我在学习的过程中发现很多地方都关联到了对象头的知识点,例如JDK中的 synch
- 本文实例为大家分享了C#实现飞行棋的具体代码,供大家参考,具体内容如下基于Winform框架写的不足之处请大佬指教using System;
- 最近IDEA打可执行Jar包搞了三天,一直失败,好好学习一下Maven-assembly,在此记录一下1. 需求项目打包,满足以下要求:1.
- mybatis-plus-generator + clickhouse 自动生成代码依赖<!--> mybatis-plus &
- 一、线程间等待与唤醒机制wait()和notify()是Object类的方法,用于线程的等待与唤醒,必须搭配synchronized 锁来使
- 项目记录:1.图像原理通常图像都是2D,对一副图像,可以看做其宽w*高h的一个二维数组, 即 图像=int[w][h],在w和h位置的每一个
- 在消息通知的时候,我们经常用到两个控件Notification和Toast。特
- 一、前端搭建1、前端用到js:uploadify(下载地址:http://www.uploadify.com/download/)、laye
- 问题描述我在接受 mq 消息的时候,需要做一个重试次数限制,如果超过 maxNum 就发邮件告警,不再重试。所以我需要对 consumer