使用 Spring 将内容从 SQL/DAO 流式传输到文件中的浏览器

问题描述 投票:0回答:1

所以我遇到了这个问题,我需要将巨大的 SQL 结果(数百万行)作为文件返回给客户端。 为了解决这些问题,我将它们分成两部分:

  1. 以不会出现 OOM 错误的方式将 SQL 结果从 DAO 流式传输到服务层。
  2. 将从 DAO(本质上是 Stream)对象获取的数据作为文件流式传输到客户端。

技术堆栈:SpringMVC、Java11、PostgreSQL、JdbcTemplate、Hibernate。

为了解决第一点,我使用了jdbcTemplate,因为提到的一些资源hibernate的scrollableResults对于PostgreSQL来说内存不友好。 JdbcTemplate.queryForStream() 似乎是正确的选择。

    String query = "select name from usersTable limit 1000000";
    jdbcTemplate.setFetchSize(10_000);
    
    Stream<String> revs = jdbcTemplate.queryForStream(query,
        (resultSet, rowNum) ->
            resultSet.getString("name"));


    return ResponseEntity
        .status(HttpStatus.OK)
        .contentType(MediaType.valueOf(MediaType.MULTIPART_FORM_DATA_VALUE))
        .body(new InputStreamResource(IOUtils.toInputStream(String.join("\n", revs.collect(Collectors.toList())))));

资源:https://jvmaware.com/streaming-json-response/

为了解决第二点,我使用的是 ResponseEntitiy,这是几乎每个人都推荐的。

API 似乎按预期很好地发回了文件。

但是,当我查看 jconsole 时,我看到堆内存不断上升,直到文件被发送,并且在文件发送到浏览器时,出现了更大的峰值。我预计该流会被 InputStream 隐式刷新,并且不会导致任何高内存使用问题。

有人可以指出做这两件事的正确方法吗?

如有任何帮助,我们将不胜感激。

谢谢

我尝试了

JdbcTemplate.queryForStream()
,但还没有机会测试它是否有效,因为我现在专注于从服务层传输大结果。

我尝试写入 HttpServletResponse.getOutputStream() 但同样的内存峰值。

我假设一旦写入输出流就必须刷新它。但是出现了这个错误。

2024-02-29 20:14:23,775 ERROR [com.sample.controller.exception.ApplicationExceptionHandler] (default task-2) Exception Occurred : org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.sample.service.impl.UserServiceImpl$$Lambda$858/0x0000000801578840] with preset Content-Type 'multipart/form-data'

相同的代码:

String query = "select name from usersTable limit 1000000";
    jdbcTemplate.setFetchSize(10_000);
    
    Stream<String> revs = jdbcTemplate.queryForStream(query,
        (resultSet, rowNum) ->
            resultSet.getString("name"));


StreamingResponseBody responseBody = httpResponseOutputStream ->
        revs.forEachOrdered(row -> {
          try {
            IOUtils.copy(IOUtils.toInputStream(row), httpResponseOutputStream);
            httpResponseOutputStream.flush();
          } catch (IOException e) {
            throw new RuntimeException(e);
          }
        });

    return ResponseEntity
        .status(HttpStatus.OK)
        .cacheControl(CacheControl.noCache())
        .contentType(MediaType.valueOf(MediaType.MULTIPART_FORM_DATA_VALUE))
        .body(responseBody);

不确定我是否缺少任何配置。

java spring java-stream httpresponse response-entity
1个回答
0
投票

这个问题的解决方案是使用老式 JDBC。

  1. 首先要解决DAO数据收集问题,从你的
    Connection
    中获得一个
    DataSource
  2. 在执行查询之前将 autoCommit 设置为 false(可能是黑客行为,但据我了解,如果 autoCommit 为 true,它将加载整个结果集并忽略
    FetchSize
    )。
  3. 将您的查询创建为
    Statement
    /
    PreparedStatement
    /...
  4. 将您的声明的
    FetchSize
    设置为应用程序性能最佳的值(50,000 对我来说有效)。
  5. 执行您的查询,它将获取
    ResultSet
  6. 循环
    ResultSet
    并继续写入每一行(如果需要的话,在任何转换之后。将其转换为
    String
    或任何
    byte[]
    able 对象)
  7. 上面变换后的对象可以写入
    ServletOutputStream
    HttpServletResponse

就是这样!!

注意:

  • 由于您将DAO结果写入outputStream,因此您必须将outputStream对象带入DAO中。检查它是否适合您的用例。
  • 在我的测试中,200 万行的流式传输完美无缺,没有出现 OOM 错误。当 JDBC 隐式刷新每个批次时,头内存会发生波动。
  • CPU 使用率出现小峰值。我想这就是我可以接受的权衡。
  • 这种流式传输方式不应取决于您的数据库/驱动程序版本。
  • 使用
    try with resources
    轻松管理资源。
  • 最重要的是,您不必写入任何辅助存储设备。

干杯!

© www.soinside.com 2019 - 2024. All rights reserved.