Jdbc流式读取及后续流式处理的正确方式及示例

news/2024/7/9 21:16:35 标签: jdbc stream, 数据库, java, java-ee, postgresql

文章目录

  • PostgreSQL中开启流式读取
  • 为什么开启了流式读取后依然OOM?
  • 流式读取并流式写入文件
  • 流式读取并流式返回给前端JSON对象
  • 测试
  • 其他有帮助文章和代码
  • 示例代码

本文数据库使用PostgreSQL,如果是MySQ的话请移步 MySQL JDBC的官方文档

PostgreSQL中开启流式读取

官方文档

为什么开启了流式读取后依然OOM?

大部分同学都知道在JDBC查询的时候,有的数据库驱动会把查询的数据全部返回给程序。这样就很容易造成OOM。下列内容来自PostgreSQL JDBC文档

By default the driver collects all the results for the query at once. This can be inconvenient for large data sets so the JDBC driver provides a means of basing a ResultSet on a database cursor and only fetching a small number of rows.

大部分同学也都知道,为了不一次性把数据加载到程序中,可以使用流式读取的方式,就是两行代码,关键是设置fetchSize

java">connection.setAutoCommit(false);
statement.setFetchSize(50);

但是流式读取的后面应该怎么返回/处理数据,很多人没说。我比较笨且思考较少,于是我在Service中写出了下列代码

java">public class JdbcStreamService {

	public List<User> getUserList() {
		List<User> result = new ArrayList();
		String url = "jdbc:postgresql://localhost:5432/postgres?user=postgres&password=12345&?currentSchema=public";
        Connection conn = DriverManager.getConnection(url);
        // 关闭自动提交
        conn.setAutoCommit(false);
        Statement st = conn.createStatement();
        // 开启流式读取
        st.setFetchSize(50);
        ResultSet rs = st.executeQuery("SELECT * FROM public.user");
        try {
            while (rs.next()) {
                int userId = rs.getInt(1);
                String password = rs.getString(2);
                String roles = rs.getString(3);
                String introduction = rs.getString(4);
                User user = new User();
                user.setUserId(userId);
                user.setPassword(password);
                user.setRoles(roles);
                user.setIntroduction(introduction);
                result.add(user);
            }
        } finally {
            rs.close();
            st.close();
            conn.close();
        }
        return result;
	}
}

很开心,我已经开启了流式读取,不用担心OOM了,我把所有结果放到List里,然后返回给Controller。但是很不幸,当启动程序之后,访问该接口,这种做法依旧会OOM。

为什么?

因为虽然我们是分批次读取的数据库结果,但是每批次生成的User对象一直没有被消费,一直放在List中,JVM不能把它GC掉。

自己想了一下,之所以写出上述代码,还是自己对理解不够深刻。数据流就像水,有源头,有目的地,数据流必须消费,所谓的消费可以理解成被放到某个存储中,或者被删除。这个存储要么是内存、要么是磁盘上的文件、要么是通过网络传给另外一端。这样理解之后,就知道该怎么写出正确代码了。

我们每次从数据库拿出一批数据,这批数据在ResultSet中被转为对应的Entity,那么我们就要在ResultSet中消费掉这个Entity流,因为默认的Entity已经在JVM内存中了,为了不引起OOM,我们就要将这个Entity写入文件中或者通过网络返回给请求方,请求方大多数情况下是给浏览器。

流式读取并流式写入文件

这里的场景是假设要生成一个报表,该报表的数据来自数据库查询结果,查询结果非常多。这是个非常常见的需求。

下面代码数据库查询使用JPA方式,设置fetchSize,循环读取该批次数据时,每次循环拿到一个User对象后,就将这个对象写入文件中,然后将该对象从persistence context移除。这里一定要detach,否则persistence context也会因为entity太多导致OOM。

java">public boolean streamJdbcResultToFile(String fileName) {
         EntityManager entityManager = JPAUtil.acquireEntityManager();
        Query<User> query = (Query<User>) entityManager.createQuery(JPQL, User.class);
        query.setFetchSize(FETCH_SIZE);
//        两种方式都可以设置FetchSize
//        query.setHint(AvailableHints.HINT_FETCH_SIZE, FETCH_SIZE);
        try (FileWriter fileWriter = getFileWriter(fileName);
             ScrollableResults<User> scrollableResults = query.scroll()) {
            while (scrollableResults.next()) {
                User user = scrollableResults.get();
                writeToFile(fileWriter, user);
                // 必须detach,否则persistence context也会因为entity太多导致OOM
                entityManager.detach(user);
            }
        }
        return true;
}

流式读取并流式返回给前端JSON对象

这里的场景是将大量JSON对象以数据流行式返回给前端。

返回给前端的JSO数据结构类似于这样

{
	"users": [{
		"userId": 1,
		"password": "KRRLAZg0IzxPY232lJefu9l6Hts8HO1cTfLIF38jrqPWNjIT78nVYlNrCsOl",
		"roles": "a5cdt8YMmXdxc5jkwFKxJBvkCZMR25ljGvVr79h33R0rB1SQKoIm6AllSGVL5Xk119phqMPKvYSPvxkXkc9W0PClhAybnPNGKm9jGey6P8IuisUNP5xvDZpKuPj00kyQ9lKSU6zr5qJN1i5U0dhOqAqPUOqpuluZNdwtDuVkaFI8sqFhCdYO6bUtSMCbiuyAOzFkn05t",
		"introduction": "8m1IXJOmixm2joDScCW2LVZwJtsNdBuG3NUzlMCMjtlYnvMJ6SzEkxRATmbq4mcb7WQ1NPCWPDjKsGfAnZpewihL7Ih95IxpGzcN8vq58m9DRZjiQyIhS7DrH60chEUuEV2qaf80hxq7p3P8gmPRXTtidt9lVOT7fhj0hN0SMqJtnyXoaQ6WFOVXehGkQyZfjWAJJiyu"
	}]
}

这里使用的是RestEasy的响应流,如果是纯Servlet的,可以换成ServletOutputStream

java">public StreamingOutput streamJdbcResultToResponse() {
     StreamingOutput stream = output -> {
        EntityManager entityManager = JPAUtil.acquireEntityManager();
        Query<User> query = (Query<User>) entityManager.createQuery(JPQL, User.class);
        query.setFetchSize(FETCH_SIZE);
        // 两个流都会被自动关闭
        try(ScrollableResults<User> scrollableResults = query.scroll();
        // 使用JsonGenerator
            JsonGenerator jsonGenerator = OBJECT_MAPPER.getFactory().createGenerator(output);) {
            // 开始构造JSON对象
            jsonGenerator.writeStartObject();
            // JSON对象的key
            jsonGenerator.writeArrayFieldStart("users");

            while (scrollableResults.next()) {
                User user = scrollableResults.get();
                jsonGenerator.writeObject(user);
                entityManager.detach(user);
            }
            // 结束
            jsonGenerator.writeEndArray();
            jsonGenerator.writeEndObject();
        }
    };
    return stream;
}

测试

测试的时候,可以将fetchSize相关代码放开或注释掉,并在IDEA中设置较小的JVM内存,例如

-Xms50m -Xmx50m

观察OOM的发生以及在较小内存下,流式读取+流式处理结果集,写文件、写响应给前端的不同之处。

其他有帮助文章和代码

本文示例代码没有使用Spring,关于在Spring中将结果流式返回给前端,可以参考下面这篇文章或去Stackoverflow上找一下

  • stream json response

示例代码

源码中包括了一个完整的PostgreSQL的docker-compose文件,以及生成测试数据的配置。想要完整体验代码的同学,确保本地已经安装了Docker,JDK11, Tomcat10即可。如何运行、如何测试都已在README中给出。

  • 示例代码仓库

http://www.niftyadmin.cn/n/5418399.html

相关文章

Linux多线程之线程控制

(&#xff61;&#xff65;∀&#xff65;)&#xff89;&#xff9e;嗨&#xff01;你好这里是ky233的主页&#xff1a;这里是ky233的主页&#xff0c;欢迎光临~https://blog.csdn.net/ky233?typeblog 点个关注不迷路⌯▾⌯ 目录 一、pthread_crate 二、pthread_join 三、p…

图像处理与图像分析—图像的读入(C语言)

学习将会依据教材图像处理与图像分析基础&#xff08;C/C&#xff09;版内容展开 什么是数字图像处理 一副图像可以定义为一个二维函数 f(x&#xff0c;y) &#xff0c;其中 x 和 y 是空间&#xff08;平面&#xff09;坐标&#xff0c;任意一对空间坐标 (x,y) 处的幅度值 &am…

[LeetCode][LCR149]彩灯装饰记录 I——二叉树的层序遍历

题目 LCR 149. 彩灯装饰记录 I 给定一棵圣诞树&#xff0c;记作根节点为 root 的二叉树&#xff0c;节点值为该位置装饰彩灯的颜色编号。按照从左到右的顺序返回每一层彩灯编号。 示例 1&#xff1a; 输入&#xff1a;root [8,17,21,18,null,null,6] 输出&#xff1a;[8,17,…

element-ui plus v2.60 终于修复了 radio checkbox 取值不明确的问题,label value 值

element-ui plus v2.60 终于修复了 radio checkbox 取值不明确的问题&#xff0c;label value 值 昨天想反馈这个关于 label 和 value 的问题的时候&#xff0c;发现新版本已经修正这个问题了。 一、ElementUI 旧版关于 label 和 value 的问题 从之前 element-ui 用过来的&a…

Day29:安全开发-JS应用DOM树加密编码库断点调试逆向分析元素属性操作

目录 JS原生开发-DOM树-用户交互 JS导入库开发-编码加密-逆向调试 思维导图 JS知识点&#xff1a; 功能&#xff1a;登录验证&#xff0c;文件操作&#xff0c;SQL操作&#xff0c;云应用接入&#xff0c;框架开发&#xff0c;打包器使用等 技术&#xff1a;原生开发&#x…

STM32基本定时功能

1、定时器就是计数器。 2、怎么计数&#xff1f; 3、我们需要有一恒定频率的方波信号&#xff0c;再加上一个寄存器。 4、比如每来一个上升沿信号&#xff0c;寄存器值加1&#xff0c;就可以完成计数。 5、假设方波频率是100Hz&#xff0c;也就是1秒100个脉冲。…

【DevOps基础篇之k8s】Kubernetes API服务认证/授权

【DevOps基础篇之k8s】Kubernetes API服务认证/授权 目录 【DevOps基础篇之k8s】Kubernetes API服务认证/授权Kubernetes API ServerAPI服务器主机和端口API服务器认证/授权基于SSL证书的认证基于令牌的认证推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课Kube…

CentOS 7 devtoolset编译addressSanitizer版本失败的问题解决

在我的一个Cent OS7开发环境中&#xff0c;按https://yeyongjin.blog.csdn.net/article/details/134178420的方法升级GCC版本到8.3.1。 这两天&#xff0c;要用Google的addressSanitizer检验内存问题&#xff0c;加上编译参数后&#xff0c;却发现编译不通过。configure时直接退…