You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring下载中继接口致同浏览器会话阻塞问题求助

问题描述

我开发了一个基础Spring应用,它接收用户提供的URL,后台通过Jsoup和Playwright获取特定下载链接。返回的是MP4格式链接,移动端无法自动下载,且因是外部链接,download属性无效,因此我实现了一个下载中继接口。本地运行正常,但部署到VPS(采用Tomcat+Nginx反向代理,也尝试过Docker化)后,在同浏览器会话中,需等待下载完成才能访问该域名及所有子域名;隐身窗口或其他浏览器则无此问题,iOS端Chrome新开标签可正常下载其他视频。我已尝试禁用缓存、缓冲等操作,但问题仍存在。

相关代码及配置如下:

下载接口代码

@GetMapping("/download")
public void downloadVideo(@RequestParam("url") String videoUrl, HttpServletResponse response) throws IOException {
    try (CloseableHttpClient client = HttpClients.createDefault()) {
        HttpGet request = new HttpGet(videoUrl);
        try (ClassicHttpResponse httpResponse = client.executeOpen(null, request, null)) {

            int statusCode = httpResponse.getCode();

            if (statusCode == HttpStatus.SC_OK) {
                HttpEntity entity = httpResponse.getEntity();
                if (entity != null) {
                    String fileName = extractFileName();
                    response.setContentType("application/octet-stream");
                    response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
                    response.setHeader("Content-Length", String.valueOf(entity.getContentLength()));

                    try (InputStream inputStream = entity.getContent();
                         OutputStream outputStream = response.getOutputStream()) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = inputStream.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, bytesRead);
                        }
                        outputStream.flush();
                    } finally {
                        EntityUtils.consume(entity);
                    }
                }
            } else {
                throw new IOException("Failed to download video. Status code: " + statusCode);
            }
        }
    }
}

HTML代码

<a class="btn btn-primary download-button" target="_blank"
                           th:href="@{~/download(url=${video.videoPath})}" th:download="${video.title}+.mp4">Download video</a>

Nginx反向代理配置

#PROXY-START/

location ^~ /
{
    proxy_pass http://127.0.0.1:8091;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_http_version 1.1;
    # proxy_hide_header Upgrade;
    #Persistent connection related configuration

    add_header X-Cache $upstream_cache_status;
    #Set Nginx Cache


    if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" )
    {
        expires 1m;
    }
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    proxy_cache cache_one;
    proxy_cache_key $host$uri$is_args$args;
    proxy_cache_valid 200 304 301 302 1m;
}
#PROXY-END/

解决方案

1. 异步化下载接口,释放Tomcat线程

同步下载会持续占用Tomcat工作线程,导致同会话后续请求排队等待。改用Spring的StreamingResponseBody结合异步HTTP客户端,实现非阻塞下载:

@GetMapping("/download")
public ResponseEntity<StreamingResponseBody> downloadVideo(@RequestParam("url") String videoUrl) {
    StreamingResponseBody responseBody = outputStream -> {
        try (AsyncHttpClient client = Dsl.asyncHttpClient()) {
            client.prepareGet(videoUrl)
                  .execute()
                  .toCompletableFuture()
                  .thenAccept(response -> {
                      try (InputStream inputStream = response.getResponseBodyAsStream()) {
                          byte[] buffer = new byte[8192];
                          int bytesRead;
                          while ((bytesRead = inputStream.read(buffer)) != -1) {
                              outputStream.write(buffer, 0, bytesRead);
                              outputStream.flush();
                          }
                      } catch (IOException e) {
                          throw new RuntimeException(e);
                      }
                  })
                  .join();
        } catch (IOException e) {
            throw new RuntimeException("Failed to download video", e);
        }
    };

    String fileName = extractFileName();
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
            .body(responseBody);
}

2. 调整Tomcat线程池配置

修改application.properties,增加工作线程数并缩短连接超时,避免线程耗尽:

server.tomcat.max-threads=200
server.tomcat.min-spare-threads=20
server.tomcat.connection-timeout=20000

3. 优化Nginx配置,排除下载接口的缓存

单独为/download路径配置反向代理,禁用缓存避免连接阻塞:

location ^~ /download {
    proxy_pass http://127.0.0.1:8091;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
    proxy_cache off;
    proxy_ignore_headers Cache-Control expires;
}

location ^~ / {
    # 保留原有的其他配置
    proxy_pass http://127.0.0.1:8091;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_http_version 1.1;

    add_header X-Cache $upstream_cache_status;

    if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" )
    {
        expires 1m;
    }
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    proxy_cache cache_one;
    proxy_cache_key $host$uri$is_args$args;
    proxy_cache_valid 200 304 301 302 1m;
}

同时调整Nginx全局连接参数,提升并发能力:

events {
    worker_connections 10240;
    use epoll;
}

http {
    proxy_connect_timeout 30s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
    keepalive_timeout 65s;
    keepalive_requests 100;
    # 其他原有配置...
}

4. 前端改用JS触发下载,避免会话连接阻塞

通过JavaScript发起下载请求,不占用原页面的会话连接:

<a class="btn btn-primary download-button" 
   onclick="triggerDownload('[[${video.videoPath}]]', '[[${video.title}]]')">Download video</a>

<script>
function triggerDownload(url, title) {
    fetch('/download?url=' + encodeURIComponent(url))
        .then(response => response.blob())
        .then(blob => {
            const a = document.createElement('a');
            const blobUrl = URL.createObjectURL(blob);
            a.href = blobUrl;
            a.download = title + '.mp4';
            document.body.appendChild(a);
            a.click();
            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(blobUrl);
            }, 0);
        })
        .catch(err => console.error('下载失败:', err));
}
</script>

内容的提问来源于stack exchange,提问作者John Doe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 01:00:55