前端切片(SpringBoot上传GB级大文件卡死?“分块切割术”让速度飙升10倍!)

前端切片(SpringBoot上传GB级大文件卡死?“分块切割术”让速度飙升10倍!)
SpringBoot上传GB级大文件卡死?“分块切割术”让速度飙升10倍!

一、场景重现:100MB 成了“生死线”?

在做项目的过程中,你一定遇到过这种窘境: 用户传个 1GB 的安装包,Nginx 直接 504 Gateway Timeout,或者 Tomcat 的临时目录瞬间爆满,甚至直接甩你一个 java.lang.OutOfMemoryError: Java heap space。

传统的 MultipartFile 一次性接收模式,在面对 GB 级大文件时,本质上是在跟服务器内存和网络稳定性“赌命”。一旦网络抖动 1 秒,之前的 99% 全部白传。

前端切片(SpringBoot上传GB级大文件卡死?“分块切割术”让速度飙升10倍!)

要解决这个问题,必须把“大石头”敲碎成“小石子”,这就是我们要聊的——分块切割术

二、 核心原理:前后端握手协议

一个工业级的分块上传流程通常包含三个动作:

  1. Check (秒传/续传探测): 发送文件 MD5,后端返回:“这个文件我有了(秒传)” 或 “我只有第 1, 3, 5 块(续传)”。
  2. Upload (分块传输): 前端并发推送缺失的切片。
  3. Merge (请求合并): 前端通知后端:“碎片齐了,拼起来吧”。

三、 后端硬核实现:SpringBoot + RandomAccessFile

1. 存储结构设计

我们拒绝“一次性加载到内存”,使用 RandomAccessFile 直接在磁盘指定偏移量写入,这能极大地节省内存。

@Datapublic class ChunkDTO {    private String fileMd5;   // 文件唯一标识    private Integer chunkIndex; // 当前分块序号    private Long chunkSize;    // 分块大小    private MultipartFile file; // 实际二进制数据}

2. 分块上传与偏移量写入

不要等全部传完再合并!我们在收到每一块时,直接定位到它在最终文件中的位置并写入。这样可以避免最后合并时产生巨大的 IO 瓶颈。

@RestController@RequestMapping("/api/upload")public class FileUploadController {    private final String UPLOAD_PATH = "/data/uploads/temp/";    @PostMapping("/chunk")    public ResponseEntity uploadChunk(ChunkDTO chunkDTO) throws IOException {        // 1. 创建临时分块目录或直接操作目标文件        File targetFile = new File(UPLOAD_PATH + chunkDTO.getFileMd5() + ".tmp");        try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {            // 2. 关键:计算偏移量(当前块序号 * 标准块大小)            long offset = chunkDTO.getChunkIndex() * chunkDTO.getChunkSize();            raf.seek(offset);            raf.write(chunkDTO.getFile().getBytes());        }        // 3. 记录已上传的索引(可存入 Redis)        redisTemplate.opsForSet().add("uploaded_chunks:" + chunkDTO.getFileMd5(), chunkDTO.getChunkIndex());        return ResponseEntity.ok("Chunk " + chunkDTO.getChunkIndex() + " saved.");    }}

3. 生产级合并逻辑

合并不仅仅是重命名,还需要校验完整性(MD5 重算)以及正式存储(如移至 MinIO 或云存储)。

@PostMapping("/merge")public ResponseEntity mergeFile(@RequestParam String fileMd5, @RequestParam String fileName) {    File tmpFile = new File(UPLOAD_PATH + fileMd5 + ".tmp");    File finalFile = new File("/data/uploads/final/" + fileName);    // 1. 检查分块是否完整(对比 Redis 中的 Count 和前端切片总数)    if (!checkIntegrity(fileMd5)) {        return ResponseEntity.status(500).body("分块缺失,请重传");    }    // 2. 原子性重命名    if (tmpFile.renameTo(finalFile)) {        // 3. 清理 Redis 记录        redisTemplate.delete("uploaded_chunks:" + fileMd5);        return ResponseEntity.ok("文件上传并合并成功");    }    return ResponseEntity.status(500).body("合并失败");}

四、 前端实战:Vue + 并发控制

前端不能一股脑把 100 个请求全发出去,否则浏览器会卡死。我们需要一个简单的并发队列

// 核心切片逻辑async function handleUpload(file) {    const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB    const chunks = [];    const fileMd5 = await calculateMD5(file); // 使用 spark-md5 库    // 1. 预检:哪些块已经上传过了?    const { data: uploadedList } = await axios.get(`/check?md5=${fileMd5}`);    // 2. 切片    for (let i = 0; i < file.size; i += CHUNK_SIZE) {        chunks.push({            file: file.slice(i, i + CHUNK_SIZE),            index: i / CHUNK_SIZE        });    }    // 3. 过滤已上传块并执行并发上传    const uploadTasks = chunks        .filter(chunk => !uploadedList.includes(chunk.index))        .map(chunk => {            const formData = new FormData();            formData.append('file', chunk.file);            formData.append('chunkIndex', chunk.index);            formData.append('fileMd5', fileMd5);            formData.append('chunkSize', CHUNK_SIZE);            return axios.post('/api/upload/chunk', formData);        });    // 4. 全部完成后触发合并    await Promise.all(uploadTasks);    await axios.post('/api/upload/merge', { fileMd5, fileName: file.name });}

五、 避坑与优化(生产环境必看)

  1. Redis 状态预检查: 在 uploadChunk 之前,前端必须先调 check 接口。如果 Redis 显示该 fileMd5 对应的分块已满,后端直接返回“秒传成功”,不走任何 IO。
  2. 清理僵尸切片: 很多用户传到一半就关了浏览器。建议写个定时任务(Spring Schedule),清理 UPLOAD_PATH 下超过 48 小时未变动的 .tmp 文件,释放磁盘。
  3. 零拷贝优化: 如果是在 Linux 环境且不使用 RandomAccessFile,可以尝试 FileChannel.transferTo,这是真正的内核级拷贝,效率更高。

六、性能实测:不是所有分块都叫提速

我们在 1Gbps 内网环境下,对 10GB 文件的测试数据如下:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章