企业级大文件分片上传完整后端技术方案设计(基于 Redis + MinIO)

方案简介

本方案专为企业级大文件(视频、音频、安装包等)上传场景设计,基于 Spring Boot + Redis + MinIO 实现分片上传、断点续传、秒传、自动合并、自动清理一体化能力。核心解决:网络不稳定导致上传失败、大文件占用服务器资源、重复上传浪费带宽、上传中断必须重传等问题。

整体流程:前端切片 → Redis 记录分片状态 → MinIO 存储分片 → 后端校验合并 → 清理临时资源


前后端业务流程梳理

1. 上传准备与秒传判断

  1. 前端选择文件,计算整个文件 MD5(唯一标识)
  2. 按固定大小(默认 5MB)切割文件,计算每个分片 MD5
  3. 调用检查接口,传入:文件总 MD5、分片索引、分片 MD5
  4. 后端查询 Redis:
  • 分片已存在且 MD5 一致 → 前端跳过上传
  • 分片不存在 → 执行上传

2. 分片上传流程

  1. 前端上传分片 + 元数据(总 MD5、索引、起止位置、总分片数等)
  2. 后端接收分片,直接上传至 MinIO(chunks/ 目录)
  3. 后端将分片信息存入 Redis Hash 结构:
  • 分片 MD5、存储路径、起止位置
  • 总分片数、总文件大小(仅 0 号分片记录)

上传成功返回结果,支持并发分片上传

3. 合并前校验流程

  1. 前端所有分片上传完成,请求合并接口
  2. 后端执行强校验:
  • 校验总分片数是否完整
  • 校验所有分片 MD5 记录是否存在
  • 校验所有分片在 MinIO 中真实存在

校验不通过 → 拒绝合并;校验通过 → 进入合并流程

4. 分片合并流程

  1. 分片索引顺序(0、1、2…) 从 MinIO 读取分片
  2. 写入服务器本地临时文件,完成文件合并
  3. 将合并后的完整文件上传至 MinIO 正式目录(merged/
  4. 记录完整文件路径到 Redis
  5. 自动删除 MinIO 分片
  6. 自动清理 Redis 临时分片数据
  7. 返回文件可访问路径

5. 异常与重试流程

  • 上传中断:重传时仅上传缺失分片
  • 合并失败:不清理数据,支持重新合并
  • 网络波动:分片下载自带 3 次重试机制
  • 重复文件:相同 MD5 直接返回已上传路径

技术架构

架构组成

  • 前端:文件切片、MD5 计算、并发上传、断点续传控制
  • 应用服务:Spring Boot(接口实现、校验、合并、业务逻辑)
  • 状态存储:Redis(记录分片信息、文件元数据、上传状态)
  • 文件存储:MinIO(存储分片文件 + 完整文件)
  • 业务集成:MV / 音乐视频上传、大文件统一管理

核心技术栈

  • 后端框架:Spring Boot
  • 缓存中间件:Redis
  • 对象存储:MinIO
  • 校验算法:MD5(文件 + 分片双重校验)
  • 业务适配:MV 视频、音频、大文件上传

Redis 存储设计

文件总 MD5 作为 Redis Key,使用 Hash 结构存储所有信息:

Redis 字段

含义

chunk_md5_{索引}

分片 MD5 值

chunk_location_{索引}

分片 MinIO 存储路径

chunk_start_end_{索引}

分片在文件中的起止位置

file_chunks

文件总分片数

file_size

文件总大小

file_location

合并后完整文件路径


MinIO 存储结构

  • 分片文件:chunks/{分片MD5}_{分片索引}
  • 完整文件:merged/{MD5前2位}/{MD5第3-4位}/{文件名}

核心功能实现

1. 分片检查(断点续传 / 秒传核心)

/**
 * 检查当前分片是否已经成功上传(断点续传 + 秒传核心方法)
 *
 * @param fileMd5   整个文件的唯一MD5值(Redis的主Key)
 * @param chunk     当前分片的索引序号(0、1、2...)
 * @param chunkMd5  当前分片的MD5值(用于校验一致性)
 * @return boolean  true=分片已存在且MD5一致(可跳过上传);false=分片不存在或MD5不匹配(需要上传)
 */
@Override
public boolean check(String fileMd5, String chunk, String chunkMd5) {
    // 1. 获取该文件在Redis中存储的所有分片信息(仅用于调试日志,不影响逻辑)
    Map<Object, Object> allEntries = redisTemplate.opsForHash().entries(fileMd5);

    // 2. 从Redis中获取当前分片对应的MD5值
    // Redis Key = 文件MD5
    // Redis Field = chunk_md5_0、chunk_md5_1...
    Object o = redisTemplate.opsForHash().get(fileMd5, REDIS_CHUNK_MD5_PREFIX + chunk);

    // 3. 如果Redis中没有这条记录 → 表示分片未上传 → 返回false
    if (o == null) {
        return false;
    }

    // 4. 清理Redis中存储的MD5字符串(去除可能存在的双引号、空格)
    String storedChunkMd5 = o.toString().replace("\"", "").trim();

    // 5. 比对前端传入的分片MD5与Redis存储的MD5是否一致
    // 一致 → 返回true(分片已上传)
    // 不一致 → 返回false(需要重新上传)
    return chunkMd5.equals(storedChunkMd5);
}

2. 分片上传

/**
 * 单独分片上传核心方法
 * 将单个分片文件上传到 MinIO,并将分片元信息存入 Redis
 *
 * @param md5Value      整个大文件的唯一MD5值(作为Redis的Key)
 * @param chunkIndex    当前分片的索引(从 0 开始,0、1、2...)
 * @param start         分片在原文件中的起始字节位置
 * @param end           分片在原文件中的结束字节位置
 * @param chunks        大文件总分片数量
 * @param fileSize      大文件总大小(字节)
 * @param chunkMd5      当前分片文件的MD5值
 * @param multipartFile 前端上传的分片文件对象
 * @return 上传结果提示字符串
 * @throws IOException 上传或IO异常时抛出
 */
@Override
public String upload(String md5Value, String chunkIndex, String start, String end,
                     String chunks, String fileSize, String chunkMd5, MultipartFile multipartFile) throws IOException {

    // 1. 构建分片在 MinIO 中的存储路径:chunks/分片MD5_分片索引
    String chunkPath = MINIO_CHUNKS_PREFIX + chunkMd5 + "_" + chunkIndex;

    // 2. 调用 MinIO 服务,上传分片文件,并获取文件的访问路径
    String storedPath = minIoService.uploadChunkFile(multipartFile, chunkPath);

    // 3. 将分片的关键信息存入 Redis(Hash结构)
    // 存储分片在 MinIO 的访问路径
    redisTemplate.opsForHash().put(md5Value, REDIS_CHUNK_LOCATION_PREFIX + chunkIndex, storedPath);
    // 存储分片在原文件中的起止位置
    redisTemplate.opsForHash().put(md5Value, REDIS_CHUNK_START_END_PREFIX + chunkIndex, start + "_" + end);
    // 存储当前分片的 MD5 值,用于校验和断点续传
    redisTemplate.opsForHash().put(md5Value, REDIS_CHUNK_MD5_PREFIX + chunkIndex, chunkMd5);

    // 4. 仅当上传【第一个分片(索引0)】时,记录整个文件的基础信息
    // 避免重复写入,保证文件总信息只存一份
    if ("0".equals(chunkIndex)) {
        // 存储文件总大小
        redisTemplate.opsForHash().put(md5Value, REDIS_FILE_SIZE, fileSize);
        // 存储文件总分片数
        redisTemplate.opsForHash().put(md5Value, REDIS_FILE_CHUNKS, chunks);
    }

    // 5. 返回上传成功标识
    return "分片上传成功";
}

3. 合并前预检

/**
 * 合并分片之前的【完整性预检】
 * 作用:确保所有分片都已上传、Redis信息完整、MinIO文件真实存在,只有全部满足才能合并
 *
 * @param key 文件的MD5值(Redis的key)
 * @return true=所有分片完整可合并;false=缺少分片或文件异常
 */
private boolean checkBeforeMerge(String key) {
    // 1. 获取当前文件在Redis中存储的【所有分片元数据】(Hash结构全部键值对)
    Map map = redisTemplate.opsForHash().entries(key);

    // 2. 获取【文件总分片数】,如果没有记录说明上传未初始化,直接返回false
    Object file_chunks = map.get(REDIS_FILE_CHUNKS);
    if (file_chunks == null) {
        return false;
    }

    // 3. 将总分片数转为int,作为最终校验标准
    int totalChunks = Integer.parseInt(file_chunks.toString());

    // 4. 定义三个校验计数器
    int md5Count = 0;         // 已记录MD5的分片数量
    int locationCount = 0;    // 已记录存储路径的分片数量
    int fileExistsCount = 0;  // MinIO中【真实存在】的分片文件数量

    // 5. 遍历Redis中所有的分片信息,逐项校验
    for (Object hashKey : map.keySet()) {
        String keyStr = hashKey.toString();

        // 统计:有多少个分片保存了MD5
        if (keyStr.startsWith(REDIS_CHUNK_MD5_PREFIX)) {
            md5Count++;
        }

        // 统计:有多少个分片保存了路径,并校验MinIO文件是否真实存在
        if (keyStr.startsWith(REDIS_CHUNK_LOCATION_PREFIX)) {
            locationCount++;
            // 获取分片存储路径
            String chunkPath = map.get(hashKey).toString();
            // 从URL中提取MinIO真实对象名
            String objectName = extractObjectNameFromUrl(chunkPath);
            // 校验:分片在MinIO真实存在才计数
            if (minIoService.isObjectExists(objectName)) {
                fileExistsCount++;
            }
        }
    }

    // 6. 最终严格校验:三项必须全部等于【总分片数】才允许合并
    // 1) 所有分片都有MD5
    // 2) 所有分片都有存储路径
    // 3) 所有分片在MinIO上真实存在
    return totalChunks == md5Count
            && totalChunks == locationCount
            && totalChunks == fileExistsCount;
}

4. 分片合并(完整逻辑)

/**
 * 合并所有分片,生成完整文件(核心方法)
 * 1. 先校验所有分片是否齐全
 * 2. 按顺序从 MinIO 读取分片
 * 3. 写入本地临时文件完成合并
 * 4. 上传合并后的完整文件到 MinIO 正式目录
 * 5. 记录文件路径并清理临时分片数据
 *
 * @param md5Value          大文件整体 MD5(唯一标识)
 * @param originalFilename  原始文件名
 * @return 合并后文件在 MinIO 的存储路径
 * @throws IOException 文件读取、写入、上传异常
 */
@Override
public String merge(String md5Value, String originalFilename) throws IOException {

    // ===================== 1. 合并前校验 =====================
    // 校验所有分片是否都已上传、数据是否完整
    boolean flag = this.checkBeforeMerge(md5Value);
    if (!flag) {
        // 校验不通过,抛出异常,禁止合并
        throw new IOException("切片未完全上传");
    }

    // ===================== 2. 秒传复用判断 =====================
    // 检查该文件是否已经合并过(相同 MD5 直接返回已存在路径)
    Object file_location = redisTemplate.opsForHash().get(md5Value, REDIS_FILE_LOCATION);
    if (file_location != null) {
        return file_location.toString();
    }

    // ===================== 3. 初始化合并环境 =====================
    // 获取总分片数
    int file_chunks = Integer.parseInt(redisTemplate.opsForHash().get(md5Value, REDIS_FILE_CHUNKS).toString());
    // 创建服务器本地临时文件,用于接收所有分片数据
    File tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
    OutputStream outputStream = new FileOutputStream(tempFile);

    // ===================== 4. 按顺序合并所有分片 =====================
    // 必须按 0、1、2… 顺序读取,否则文件损坏
    for (int i = 0; i < file_chunks; i++) {
        // 从 Redis 获取当前分片的存储路径
        String redisKey = REDIS_CHUNK_LOCATION_PREFIX + i;
        Object chunkPathObj = redisTemplate.opsForHash().get(md5Value, redisKey);
        String chunkPath = chunkPathObj.toString();

        // 从路径中解析出 MinIO 真实对象名称
        String objectName = extractObjectNameFromUrl(chunkPath);

        // 从 MinIO 获取分片文件流
        InputStream inputStream = minIoService.getObjectStream(objectName);

        // 缓冲区 1MB,提高读写效率
        byte[] bytes = new byte[1024 * 1024];
        int len;
        // 读取分片数据并写入本地临时文件
        while ((len = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, len);
        }

        // 关闭当前分片流
        inputStream.close();
    }

    // 所有分片写入完成,关闭文件输出流
    outputStream.close();

    // ===================== 5. 上传合并后的完整文件 =====================
    // 构建正式文件存储目录:按 MD5 前 4 位分散目录,避免单目录文件过多
    String fileDir = MINIO_MERGED_PREFIX + md5Value.substring(0, 2) + "/" + md5Value.substring(2, 4) + "/";
    String filePath = fileDir + originalFilename;

    // 将本地临时文件上传到 MinIO
    try (FileInputStream tempInputStream = new FileInputStream(tempFile)) {
        filePath = minIoService.uploadFileStream(tempInputStream, filePath, FILE_CONTENT_TYPE);
    }

    // ===================== 6. 记录信息并清理 =====================
    // 把合并后的完整文件路径存入 Redis,用于秒传与下载
    redisTemplate.opsForHash().put(md5Value, REDIS_FILE_LOCATION, filePath);

    // 删除本地临时文件
    tempFile.delete();

    // 删除 MinIO 中的分片文件 + 清理 Redis 临时数据
    delTmpFile(md5Value);

    // 返回最终文件路径
    return filePath;
}

5. 自动清理临时资源

/**
 * 清理临时文件与Redis临时数据(合并成功后调用)
 * 功能:删除MinIO里的分片文件 + 删除Redis里的分片元数据,释放存储空间
 *
 * @param md5Value 文件整体MD5值(Redis的key)
 * @throws IOException IO异常
 */
private void delTmpFile(String md5Value) throws IOException {
    // 1. 获取当前文件在Redis中存储的所有Hash数据
    Map map = redisTemplate.opsForHash().entries(md5Value);

    // 2. 定义两个集合
    List<String> list = new ArrayList<>();            // 存放需要从Redis删除的key
    List<String> chunkPaths = new ArrayList<>();      // 存放需要从MinIO删除的分片路径

    // 3. 遍历Redis所有字段,分类收集需要删除的数据
    for (Object hashKey : map.keySet()) {
        String key = hashKey.toString();

        // 处理:分片存储路径 → 收集起来用于删除MinIO文件
        if (key.startsWith(REDIS_CHUNK_LOCATION_PREFIX)) {
            String chunkPath = map.get(hashKey).toString();
            // 从URL中提取MinIO真实对象名
            String realObjectName = extractObjectNameFromUrl(chunkPath);
            chunkPaths.add(realObjectName);
            // 同时把这个key加入待删除列表
            list.add(key);
        }

        // 处理:分片起止位置信息 → 加入Redis删除列表
        if (key.startsWith(REDIS_CHUNK_START_END_PREFIX)) {
            list.add(key);
        }

        // 处理:分片MD5信息 → 加入Redis删除列表
        if (key.startsWith(REDIS_CHUNK_MD5_PREFIX)) {
            list.add(key);
        }
    }

    // 4. 批量删除MinIO中的分片文件(如果有分片需要删除)
    if (!chunkPaths.isEmpty()) {
        minIoService.batchDeleteFiles(chunkPaths);
    }

    // 5. 把文件基本信息(总分片数、总大小)也加入删除列表
    list.add(REDIS_FILE_CHUNKS);
    list.add(REDIS_FILE_SIZE);

    // 6. 批量删除Redis中所有临时数据
    redisTemplate.opsForHash().delete(md5Value, list.toArray());
}

6. 统一大文件上传接口(支持上传 / 合并自动判断)

/**
 * 大文件统一上传入口(支持分片上传 + 自动合并)
 * 前端通过 operation 字段控制操作类型:
 * upload = 上传分片
 * merge  = 合并分片
 *
 * @param fileUploadDTO 前端传递的所有文件上传参数(MD5、分片索引、文件、操作类型等)
 * @return 上传/合并结果信息
 */
@Override
public String uploadLargeFile(FileUploadDTO fileUploadDTO) {
    // 获取文件唯一标识(整个大文件的MD5)
    String md5Value = fileUploadDTO.getMd5Value();
    // 获取当前分片索引(0、1、2...)
    String chunk = fileUploadDTO.getChunk();
    // 获取操作类型:upload-上传分片 / merge-合并文件
    String operation = fileUploadDTO.getOperation();

    // ======================================
    // 操作一:执行【分片上传】
    // ======================================
    if (OPERATION_UPLOAD.equals(operation)) {
        // 先检查分片是否已经上传过(断点续传)
        boolean exists = check(md5Value, chunk, fileUploadDTO.getChunkMd5());
        // 已上传 → 直接返回,无需重复上传
        if (exists) {
            return "分片已存在";
        }
        // 未上传 → 调用真正的分片上传方法保存到 MinIO 和 Redis
        return upload(
                md5Value,
                chunk,
                fileUploadDTO.getStart(),
                fileUploadDTO.getEnd(),
                fileUploadDTO.getChunks(),
                fileUploadDTO.getSize(),
                fileUploadDTO.getChunkMd5(),
                fileUploadDTO.getFile()
        );
    }

    // ======================================
    // 操作二:执行【分片合并】
    // ======================================
    if (OPERATION_MERGE.equals(operation)) {
        // 调用合并方法,将所有分片合成完整文件
        return merge(md5Value, fileUploadDTO.getOriginalFilename());
    }

    // 未知操作,默认返回成功
    return "操作成功";
}

优化特性

  1. 秒传机制:相同 MD5 文件直接返回路径,无需上传
  2. 断点续传:网络中断后仅上传缺失分片
  3. 幂等上传:重复上传同一分片自动跳过
  4. 合并重试:合并失败不清理数据,支持重试
  5. 智能路径解析:兼容任意域名 / 路径,无硬编码
  6. 高并发支持:前端控制并发数,服务端无压力
  7. 自动垃圾回收:合并完成自动删除分片、清理 Redis

适用场景

  • MV / 短视频 / 高清视频上传
  • 音频、专辑、歌曲文件上传
  • 大文件安装包、压缩包上传
  • 企业网盘、素材库、媒体库
  • 所有需要稳定、高效、断点续传的大文件上传场景
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐