乐于分享
好东西不私藏

今日面试题:大文件下载的断点续传与容灾设计

今日面试题:大文件下载的断点续传与容灾设计

📌 今日面试题:大文件下载的断点续传与容灾设计

场景描述
你负责的一个企业网盘系统,支持用户下载平均大小为5GB的超高清视频素材。文件存储在阿里云OSS(对象存储)上。业务反馈:部分一线剪辑师在下载90%左右时,因网络波动(如Wi-Fi切换、信号抖动)导致下载失败,必须从头重下,严重影响工作效率。

面试官连环炮

1. 你会如何设计前端的大文件下载方案,来应对网络中断问题?请对比传统标签直下的弊端。
2. 如果要实现“断点续载”,前端需要利用浏览器的什么能力?后端或OSS需要提供什么支持?核心原理是什么?
3. 如果用户点击下载后,关闭了浏览器标签页,几小时后又重新点击下载同一个文件,你如何实现“秒下”(即直接从本地或缓存恢复)?
4. 下载过程中,如何验证每个分片的完整性?全部下载完成后,如何确保最终合并的文件与OSS源文件一致?
5. (进阶)如果公司自建分布式存储,没有OSS现成的分片下载能力,你会如何设计服务端的分片读取与下发逻辑?如何控制内存占用?

---—-

🔥 专业解答与思路拆解

核心都是“化整为零”+“记录进度”+“校验完整性”,但下载场景有其独特的技术侧重点。

1. 传统方案弊端与核心设计思路

· 传统标签直链弊端:浏览器原生下载不支持断点续传。一旦中断,TCP连接断开,之前下载的字节全部作废,必须从头开始。这在大文件场景下对用户体验和公司出口带宽是巨大浪费。
· 核心设计思路:HTTP范围请求 + 前端分片管理 + 本地进度持久化。
 · HTTP范围请求(Range Requests):这是断点续传的基石。服务器需支持Accept-Ranges: bytes响应头,前端通过Range: bytes=start-end请求头获取文件的指定片段。
 · 前端分片管理:将大文件拆分成多个连续的分片(如每个5MB),并发或串行下载这些分片。
 · 本地进度持久化:利用IndexedDB或本地文件系统缓存已下载的分片数据,并用Redis(后端记录) 或LocalStorage(前端记录) 记录已完成的切片索引。

2. 断点续载的实现原理

· 浏览器能力:主要利用Fetch API或XMLHttpRequest设置Range头,以及Streams API(高级特性)处理数据流。关键在于将响应数据保存到本地存储(如通过File System Access API写入本地临时文件,或暂存于IndexedDB),而不是浏览器的内存缓存。
· OSS/后端支持:
 1. 必须支持Range请求:几乎所有对象存储(OSS、S3)和静态文件服务器都支持。
 2. 提供分片下载能力:能根据前端请求的字节范围,返回对应的文件片段。
· 核心原理:
 1. 前端记录已成功下载并持久化的最大字节偏移量(或分片索引)。
 2. 断网恢复后,前端携带文件的唯一标识(如ETag或MD5)和已完成的偏移量,向后端或OSS发起查询。
 3. 后端确认文件未变更后,前端从下一个字节范围开始请求。
 · 注意:单纯依靠浏览器缓存不可靠,浏览器可能随时清理缓存。因此主动将数据持久化到本地(如通过File System Access API获得用户授权后写入本地磁盘) 或后端暂存分片是更稳健的方案。

3. “秒下”机制:本地缓存与文件指纹

用户关闭标签页再重新下载,要实现“秒下”,本质是“本地缓存命中”。

· 核心:前端在用户首次下载时,就将文件的唯一标识(通常是文件的ETag或强哈希值)与已完整下载的文件或所有分片的存储位置,关联记录在IndexedDB中。
· 流程:
 1. 用户再次点击下载,前端先根据文件ID或URL,在IndexedDB中查找是否存在对应的完整文件记录。
 2. 若存在,直接通过浏览器API(如showOpenFilePicker获取本地文件句柄)或触发本地文件读取,实现“秒下”。
 3. 若只存在部分分片,则进入“断点续载”逻辑,询问后端是否需要续传,避免重新探测。

4. 分片与最终文件的完整性校验

· 分片校验:下载每个分片时,前端可以通过请求头If-Match携带整个文件的ETag,或单独计算每个分片的数据MD5,与服务器提供的Content-MD5头(如果支持)比对,确保单个分片传输无误。
· 最终合并校验:这是必做步骤。所有分片下载并合并为完整文件后,前端(或由前端委托给Web Worker)计算整个文件的MD5/SHA256哈希值,与从服务器获取的原始文件哈希值(通过元数据接口获得)进行比对。
 · 一致:提示用户完成。
 · 不一致:说明合并过程出错或源文件变更,清理本地数据,提示用户重新下载或进入修复流程。

5. 进阶:自建存储服务端设计

如果公司没有OSS,需要自己设计文件服务接口:

· 接口设计:提供一个支持Range头的文件下载接口。使用Spring Boot等框架,利用ResponseEntity结合HttpRange对象,可以很方便地解析Range头并返回指定字节内容。
 ```java
 // 伪代码示例
 @GetMapping("/download/{fileId}")
 public ResponseEntity downloadFile(@PathVariable String fileId,
                                              @RequestHeader(value = "Range", required = false) String rangeHeader) {
     File file = getFileById(fileId); // 从存储系统获取File对象
     // 解析rangeHeader,例如 "bytes=0-1023"
     // 使用 FileSystemResource 或自定义 Resource,只读取并返回指定部分
     return ResponseEntity
             .status(rangeHeader == null ? HttpStatus.OK : HttpStatus.PARTIAL_CONTENT)
             .header("Accept-Ranges", "bytes")
             .header("Content-Range", ...)
             .body(partialFileResource);
 }
 ```
· 内存控制:绝对禁止使用FileUtils.readFileToByteArray(file)将整个文件读入内存。必须使用流式处理。通过RandomAccessFile或FileChannel定位到起始位置,然后用一个固定大小的缓冲区(如8KB)循环读取并写入OutputStream,边读边发。
· 并发控制:如果一个文件同时被多个用户下载,可以缓存文件元数据(如总大小),但每个请求的流读取是独立的,由操作系统和文件系统管理并发,服务端只需确保每个请求正确释放资源。

💡 面试避坑指南

· 不要只说前端缓存:必须强调持久化存储(IndexedDB/本地文件) 的重要性,浏览器内存缓存无法应对标签页关闭或浏览器重启。
· 不要忽略文件校验:网络传输有极小概率出错,合并时也可能因权限等问题失败,最终的哈希校验是防线。
· 不要想当然地认为所有服务器都支持Range:面试时要主动询问或提出假设,体现思维的严谨性。