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
相关产品推荐
相关产品推荐

