Java 大文件上传实战:从底层原理到分布式落地(含分片 / 断点续传 / 秒传)

Java 大文件上传实战:从底层原理到分布式落地(含分片 / 断点续传 / 秒传)

一、前言:大文件上传的痛点与解决方案

在 Java 开发中,大文件上传是高频需求也是技术难点。传统单文件直接上传方案在面对 1GB 以上文件时,常会出现超时失败、内存溢出、用户体验差等问题 —— 比如网络波动导致上传中断后需重新上传、大文件加载占用过多内存导致服务 OOM、上传进度无法感知等。

本文将从底层原理出发,结合实战场景,手把手实现一套支持分片上传、断点续传、秒传、并发控制的大文件上传方案。方案基于 JDK 17+Spring Boot 3.x 构建,整合 MyBatis-Plus、MinIO、Redis 等组件,所有代码均可直接运行,同时兼顾深度与可读性,让新手能快速上手,资深开发者能夯实底层逻辑。

二、底层原理:看透大文件上传的核心逻辑

2.1 为什么传统单文件上传行不通?

传统上传方案是将文件作为一个整体通过 HTTP 请求发送到服务端,核心问题集中在三点:

  1. 超时风险:大文件传输时间长,容易触发 HTTP 超时(默认 Tomcat 超时为 60 秒),网络波动时直接上传失败;
  2. 内存压力:服务端接收文件时,会将整个文件加载到内存处理,1GB 文件可能直接导致 JVM OOM;
  3. 体验极差:上传中断后需重新上传整个文件,无进度反馈,用户无法预估时间。

2.2 核心解决方案:分片上传

2.2.1 分片上传原理

分片上传是将大文件拆分为多个小分片(如 5MB / 片),分别上传到服务端,服务端接收完所有分片后再合并为原始文件。核心流程如下:

2.2.2 关键技术点拆解
  1. 文件唯一标识:用文件 MD5 作为唯一标识,确保分片与原始文件一一对应,也是秒传和断点续传的核心依据;
  2. 分片拆分规则:按固定大小拆分(如 5MB),最后一片大小可能小于固定值,需记录总分片数和当前分片索引;
  3. 分片传输保障:每个分片上传时携带 MD5、分片索引、总分片数等元数据,服务端校验合法性;
  4. 合并逻辑:所有分片上传完成后,按分片索引顺序合并,避免文件损坏。

2.3 断点续传与秒传的底层逻辑

2.3.1 断点续传原理

断点续传基于分片上传,核心是「记录已上传分片」,避免重复上传:

  1. 前端上传前先查询服务端:该文件已上传的分片索引列表;
  2. 前端跳过已上传分片,仅上传未完成的分片;
  3. 支持暂停 / 继续功能:暂停时仅停止分片上传,不清理已上传分片;继续时重复第一步逻辑。
2.3.2 秒传原理

秒传的核心是「文件预校验」,本质是利用文件唯一标识(MD5)快速判断文件是否已存在:

  1. 前端计算文件 MD5 后,先向服务端发送秒传校验请求;
  2. 服务端查询该 MD5 对应的文件是否已上传完成;
  3. 若已存在,则直接返回秒传成功,无需上传任何分片;
  4. 若不存在或未上传完成,则进入分片上传流程。
2.3.3 易混淆点区分
技术点 核心逻辑 适用场景
分片上传 拆分文件 + 分块传输 + 合并 所有大文件上传(100MB+)
断点续传 记录已上传分片 + 跳过重复上传 网络不稳定、大文件长时间上传
秒传 MD5 预校验 + 已上传文件匹配 重复上传同一文件(如用户二次上传)

2.4 关键技术选型说明

组件 版本号 作用说明 选择理由
JDK 17 开发运行环境 长期支持版本,兼容 Spring Boot 3.x
Spring Boot 3.2.5 项目基础框架 最新稳定版,支持 JDK 17+
MyBatis-Plus 3.5.4.4 持久层框架 简化 CRUD,支持分页、乐观锁等
MinIO 8.5.12 分布式文件存储 轻量、兼容 S3 协议,适合大文件存储
Redis 7.2.4 分布式锁、缓存已上传分片 高性能,支持分布式场景
Fastjson2 2.0.49 JSON 序列化 / 反序列化 速度快,支持 JDK 17+
Lombok 1.18.30 简化 POJO 代码 减少 getter/setter 等冗余代码
Springdoc-OpenAPI 2.2.0 Swagger3 接口文档 适配 Spring Boot 3.x,替代 Springfox
Vue 3 3.4.21 前端框架 轻量、响应式,适合上传组件开发
Axios 1.6.8 前端 HTTP 请求工具 支持中断请求、进度监听
Spark-MD5 3.0.2 前端大文件 MD5 计算 支持分片计算 MD5,避免内存溢出

三、实战准备:环境搭建与项目初始化

3.1 开发环境要求

  • JDK 17(需配置环境变量)
  • MySQL 8.0(用于存储文件元数据、分片信息)
  • Redis 7.0+(用于分布式锁、缓存)
  • MinIO 8.5+(分布式存储,可选本地存储替代)
  • Maven 3.8+(项目构建工具)
  • Node.js 16+(前端项目运行)

3.2 后端项目初始化(Spring Boot)

3.2.1 pom.xml 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>***.ken.file</groupId>
    <artifactId>large-file-upload</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>large-file-upload</name>
    <description>Java大文件上传实战项目</description>
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.4.4</mybatis-plus.version>
        <minio.version>8.5.12</minio.version>
        <fastjson2.version>2.0.49</fastjson2.version>
        <springdoc.version>2.2.0</springdoc.version>
        <guava.version>32.1.3-jre</guava.version>
    </properties>
    <dependencies>
        <!-- Spring Boot核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>***.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- 数据库驱动 -->
        <dependency>
            <groupId>***.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- MinIO -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>${minio.version}</version>
        </dependency>

        <!-- JSON处理 -->
        <dependency>
            <groupId>***.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>

        <!-- 工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>***.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <!-- Swagger3 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
3.2.2 核心配置文件(application.yml)
spring:
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/large_file_upload?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root123456
    driver-class-name: ***.mysql.cj.jdbc.Driver
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 3000ms
  # 上传文件临时存储路径
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB # 单个分片最大大小(需大于前端分片大小)
      max-request-size: 100MB # 单次请求最大大小

# Spring Boot配置
server:
  port: 8080
  servlet:
    context-path: /file-upload

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: ***.ken.file.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: isDeleted
      logic-delete-value: 1
      logic-not-delete-value: 0

# MinIO配置
minio:
  endpoint: http://localhost:9000
  a***ess-key: minioadmin
  secret-key: minioadmin
  bucket-name: large-file-bucket # 存储大文件的桶名(需提前创建)

# 自定义上传配置
file:
  upload:
    chunk-size: 5242880 # 分片大小5MB(5*1024*1024)
    local-storage-path: D:/large-file-upload/local-storage # 本地存储路径(单机模式)
    expire-days: 7 # 未完成上传的分片过期时间(天)

3.3 数据库设计(MySQL 8.0)

需创建两张核心表:file_metadata(文件元数据表)和file_chunk(分片信息表),SQL 脚本如下:

CREATE DATABASE IF NOT EXISTS large_file_upload DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE large_file_upload;

-- 文件元数据表:存储文件整体信息
CREATE TABLE IF NOT EXISTS file_metadata (
    id BIGINT AUTO_INCREMENT ***MENT '主键ID' PRIMARY KEY,
    file_md5 VARCHAR(32) NOT NULL ***MENT '文件唯一标识(MD5)',
    file_name VARCHAR(255) NOT NULL ***MENT '原始文件名',
    file_size BIGINT NOT NULL ***MENT '文件总大小(字节)',
    chunk_total INT NOT NULL ***MENT '总分片数',
    file_suffix VARCHAR(50) ***MENT '文件后缀(如mp4、zip)',
    storage_type TINYINT NOT NULL DEFAULT 1 ***MENT '存储类型:1-本地存储,2-MinIO',
    file_path VARCHAR(512) ***MENT '文件存储路径(本地路径或MinIO访问路径)',
    status TINYINT NOT NULL DEFAULT 0 ***MENT '文件状态:0-上传中,1-上传完成,2-上传失败',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ***MENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ***MENT '更新时间',
    is_deleted TINYINT NOT NULL DEFAULT 0 ***MENT '逻辑删除:0-未删除,1-已删除',
    UNIQUE KEY uk_file_md5 (file_md5) ***MENT '文件MD5唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ***MENT='文件元数据表';

-- 分片信息表:存储单个分片的信息
CREATE TABLE IF NOT EXISTS file_chunk (
    id BIGINT AUTO_INCREMENT ***MENT '主键ID' PRIMARY KEY,
    file_md5 VARCHAR(32) NOT NULL ***MENT '文件唯一标识(关联file_metadata.file_md5)',
    chunk_index INT NOT NULL ***MENT '分片索引(从0开始)',
    chunk_size BIGINT NOT NULL ***MENT '分片大小(字节)',
    chunk_path VARCHAR(512) NOT NULL ***MENT '分片存储路径',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ***MENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ***MENT '更新时间',
    is_deleted TINYINT NOT NULL DEFAULT 0 ***MENT '逻辑删除:0-未删除,1-已删除',
    UNIQUE KEY uk_file_md5_chunk_index (file_md5, chunk_index) ***MENT '文件MD5+分片索引唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ***MENT='分片信息表';

-- 索引优化:加快查询速度
CREATE INDEX idx_file_md5_status ON file_metadata(file_md5, status);
CREATE INDEX idx_file_md5 ON file_chunk(file_md5);

3.4 前端项目初始化(Vue 3)

3.4.1 项目创建与依赖安装
# 创建Vue项目
npm create vue@latest large-file-upload-frontend
cd large-file-upload-frontend
# 安装核心依赖
npm install axios@1.6.8 spark-md5@3.0.2 element-plus@2.7.0
3.4.2 前端核心配置(src/utils/request.js)
import axios from 'axios';

// 创建Axios实例
const service = axios.create({
  baseURL: 'http://localhost:8080/file-upload',
  timeout: 60000, // 分片上传超时时间(1分钟)
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 可添加token等认证信息
    return config;
  },
  error => {
    Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data;
    if (res.code !== 200) {
      console.error('请求失败:', res.msg);
      return Promise.reject(res);
    }
    return res;
  },
  error => {
    console.error('请求异常:', error.message);
    return Promise.reject(error);
  }
);

export default service;

四、核心组件开发:后端实现

4.1 实体类设计(Entity)

4.1.1 文件元数据实体(FileMetadata.java)
package ***.ken.file.entity;

import ***.baomidou.mybatisplus.annotation.IdType;
import ***.baomidou.mybatisplus.annotation.TableId;
import ***.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;

/**
 * 文件元数据表实体
 * @author ken
 */
@Data
@TableName("file_metadata")
public class FileMetadata {

    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 文件唯一标识(MD5)
     */
    private String fileMd5;

    /**
     * 原始文件名
     */
    private String fileName;

    /**
     * 文件总大小(字节)
     */
    private Long fileSize;

    /**
     * 总分片数
     */
    private Integer chunkTotal;

    /**
     * 文件后缀(如mp4、zip)
     */
    private String fileSuffix;

    /**
     * 存储类型:1-本地存储,2-MinIO
     */
    private Integer storageType;

    /**
     * 文件存储路径(本地路径或MinIO访问路径)
     */
    private String filePath;

    /**
     * 文件状态:0-上传中,1-上传完成,2-上传失败
     */
    private Integer status;

    /**
     * 创建时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    /**
     * 逻辑删除:0-未删除,1-已删除
     */
    private Integer isDeleted;
}
4.1.2 分片信息实体(FileChunk.java)
package ***.ken.file.entity;

import ***.baomidou.mybatisplus.annotation.IdType;
import ***.baomidou.mybatisplus.annotation.TableId;
import ***.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.format.annotation
转载请说明出处内容投诉
CSS教程网 » Java 大文件上传实战:从底层原理到分布式落地(含分片 / 断点续传 / 秒传)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买