SpringBoot超大文件上传(总结)

news2024/11/15 8:33:51

SpringBoot超大文件上传,秒传!分片上传!断电续传!(总结)

  • 一. 秒传
    • 1、什么是秒传
    • 2、本文实现的秒传核心逻辑
  • 二. 分片上传
    • 1、什么是分片上传
    • 2、分片上传的场景
  • 三. 断点续传
    • 1、什么是断点续传
    • 2、应用场景
    • 3、实现断点续传的核心逻辑
    • 4、实现流程步骤
      • a、方案一,常规步骤
      • b、方案二、本文实现的步骤
    • 🥈 5、分片上传/断点上传代码实现(核心)
  • 🥇 四. 后端进行写入操作的核心代码
    • 1、RandomAccessFile实现方式
    • 2、MappedByteBuffer实现方式
    • 3、文件操作核心模板类代码
  • 五. 总结

在这里插入图片描述

文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。

那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式;

一. 秒传

1、什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了。

2、本文实现的秒传核心逻辑

  • 利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位。

  • 当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径

二. 分片上传

1、什么是分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

2、分片上传的场景

  • 大文件上传

  • 网络环境环境不好,存在需要重传风险的场景

三. 断点续传

1、什么是断点续传

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

2、应用场景

断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。

3、实现断点续传的核心逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

4、实现流程步骤

a、方案一,常规步骤

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;

  • 初始化一个分片上传任务,返回本次分片上传唯一标识;

  • 按照一定的策略(串行或并行)发送各个分片数据块;

  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

b、方案二、本文实现的步骤

  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小

  • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)

  • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

🥈 5、分片上传/断点上传代码实现(核心)

前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接:

http://fex.baidu.com/webuploader/getting-started.html
后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接:

https://blog.csdn.net/dimudan2015/article/details/81910690
另一种是使用MappedByteBuffer,对MappedByteBuffer不熟悉的朋友,可以查看如下链接进行了解:

https://www.jianshu.com/p/f90866dcbffc

🥇 四. 后端进行写入操作的核心代码

1、RandomAccessFile实现方式

@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)    
@Slf4j    
public class RandomAccessUploadStrategy extends SliceUploadTemplate {    
    
  @Autowired    
  private FilePathUtil filePathUtil;    
    
  @Value("${upload.chunkSize}")    
  private long defaultChunkSize;    
    
  @Override    
  public boolean upload(FileUploadRequestDTO param) {    
    RandomAccessFile accessTmpFile = null;    
    try {    
      String uploadDirPath = filePathUtil.getPath(param);    
      File tmpFile = super.createTmpFile(param);    
      accessTmpFile = new RandomAccessFile(tmpFile, "rw");    
      //这个必须与前端设定的值一致    
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024    
          : param.getChunkSize();    
      long offset = chunkSize * param.getChunk();    
      //定位到该分片的偏移量    
      accessTmpFile.seek(offset);    
      //写入该分片数据    
      accessTmpFile.write(param.getFile().getBytes());    
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);    
      return isOk;    
    } catch (IOException e) {    
      log.error(e.getMessage(), e);    
    } finally {    
      FileUtil.close(accessTmpFile);    
    }    
   return false;    
  }    
    
}    

2、MappedByteBuffer实现方式

@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)    
@Slf4j    
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {    
    
  @Autowired    
  private FilePathUtil filePathUtil;    
    
  @Value("${upload.chunkSize}")    
  private long defaultChunkSize;    
    
  @Override    
  public boolean upload(FileUploadRequestDTO param) {    
    
    RandomAccessFile tempRaf = null;    
    FileChannel fileChannel = null;    
    MappedByteBuffer mappedByteBuffer = null;    
    try {    
      String uploadDirPath = filePathUtil.getPath(param);    
      File tmpFile = super.createTmpFile(param);    
      tempRaf = new RandomAccessFile(tmpFile, "rw");    
      fileChannel = tempRaf.getChannel();    
    
      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024    
          : param.getChunkSize();    
      //写入该分片数据    
      long offset = chunkSize * param.getChunk();    
      byte[] fileData = param.getFile().getBytes();    
      mappedByteBuffer = fileChannel    
.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);    
      mappedByteBuffer.put(fileData);    
      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);    
      return isOk;    
    
    } catch (IOException e) {    
      log.error(e.getMessage(), e);    
    } finally {    
    
      FileUtil.freedMappedByteBuffer(mappedByteBuffer);    
      FileUtil.close(fileChannel);    
      FileUtil.close(tempRaf);    
    
    }    
    
    return false;    
  }    
    
}    

3、文件操作核心模板类代码

@Slf4j    
public abstract class SliceUploadTemplate implements SliceUploadStrategy {    
    
  public abstract boolean upload(FileUploadRequestDTO param);    
    
  protected File createTmpFile(FileUploadRequestDTO param) {    
    
    FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);    
    param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));    
    String fileName = param.getFile().getOriginalFilename();    
    String uploadDirPath = filePathUtil.getPath(param);    
    String tempFileName = fileName + "_tmp";    
    File tmpDir = new File(uploadDirPath);    
    File tmpFile = new File(uploadDirPath, tempFileName);    
    if (!tmpDir.exists()) {    
      tmpDir.mkdirs();    
    }    
    return tmpFile;    
  }    
    
  @Override    
  public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {    
    
    boolean isOk = this.upload(param);    
    if (isOk) {    
      File tmpFile = this.createTmpFile(param);    
      FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);    
      return fileUploadDTO;    
    }    
    String md5 = FileMD5Util.getFileMD5(param.getFile());    
    
    Map<Integer, String> map = new HashMap<>();    
    map.put(param.getChunk(), md5);    
    return FileUploadDTO.builder().chunkMd5Info(map).build();    
  }    
    
  /**    
   * 检查并修改文件上传进度    
   */    
  public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {    
    
    String fileName = param.getFile().getOriginalFilename();    
    File confFile = new File(uploadDirPath, fileName + ".conf");    
    byte isComplete = 0;    
    RandomAccessFile accessConfFile = null;    
    try {    
      accessConfFile = new RandomAccessFile(confFile, "rw");    
      //把该分段标记为 true 表示完成    
      System.out.println("set part " + param.getChunk() + " complete");    
      //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127    
      accessConfFile.setLength(param.getChunks());    
      accessConfFile.seek(param.getChunk());    
      accessConfFile.write(Byte.MAX_VALUE);    
    
      //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)    
      byte[] completeList = FileUtils.readFileToByteArray(confFile);    
      isComplete = Byte.MAX_VALUE;    
      for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {    
        //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE    
        isComplete = (byte) (isComplete & completeList[i]);    
        System.out.println("check part " + i + " complete?:" + completeList[i]);    
      }    
    
    } catch (IOException e) {    
      log.error(e.getMessage(), e);    
    } finally {    
      FileUtil.close(accessConfFile);    
    }    
 boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);    
    return isOk;    
  }    
    
  /**    
   * 把上传进度信息存进redis    
   */    
  private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,    
      String fileName, File confFile, byte isComplete) {    
    
    RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);    
    if (isComplete == Byte.MAX_VALUE) {    
      redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");    
      redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());    
      confFile.delete();    
      return true;    
    } else {    
      if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {    
        redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");    
        redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),    
            uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");    
      }    
    
      return false;    
    }    
  }    
/**    
   * 保存文件操作    
   */    
  public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {    
    
    FileUploadDTO fileUploadDTO = null;    
    
    try {    
    
      fileUploadDTO = renameFile(tmpFile, fileName);    
      if (fileUploadDTO.isUploadComplete()) {    
        System.out    
            .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);    
        //TODO 保存文件信息到数据库    
    
      }    
    
    } catch (Exception e) {    
      log.error(e.getMessage(), e);    
    } finally {    
    
    }    
    return fileUploadDTO;    
  }    
/**    
   * 文件重命名    
   *    
   * @param toBeRenamed 将要修改名字的文件    
   * @param toFileNewName 新的名字    
   */    
  private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {    
    //检查要重命名的文件是否存在,是否是文件    
    FileUploadDTO fileUploadDTO = new FileUploadDTO();    
    if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {    
      log.info("File does not exist: {}", toBeRenamed.getName());    
      fileUploadDTO.setUploadComplete(false);    
      return fileUploadDTO;    
    }    
    String ext = FileUtil.getExtension(toFileNewName);    
    String p = toBeRenamed.getParent();    
    String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;    
    File newFile = new File(filePath);    
    //修改文件名    
    boolean uploadFlag = toBeRenamed.renameTo(newFile);    
    
    fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());    
    fileUploadDTO.setUploadComplete(uploadFlag);    
    fileUploadDTO.setPath(filePath);    
    fileUploadDTO.setSize(newFile.length());    
    fileUploadDTO.setFileExt(ext);    
    fileUploadDTO.setFileId(toFileNewName);    
    
    return fileUploadDTO;    
  }    
}    

详细内容可以具体搜素起他关键字进行深入了解;

五. 总结

在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用fastdfs、hdfs等。

本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的md5值计算,后端写入的速度还是比较快。

如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网:

https://help.aliyun.com/product/31815.html

阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。

文章内容来自:1,2,3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/187995.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ESP32 WiFi篇(六)】ESP32 WebSocket

文章目录1. WebSocket 概述2. WebSocket 的通信原理和机制3. WebSocket 与 HTTP 的关系3.1 相同点3.2 不同点3.3 关系4. WebSocket 技术出现之前&#xff0c;Web端实现即时通讯的方法有哪些&#xff1f;5. HTTP 存在的问题6. WebSocket 的特点7. WebSocket 数据帧格式1. WebSoc…

简单局域网网络故障排查和处置

简单局域网网络故障排查和处置一、了解基本网络构成1. IP传输通信图2.有线网络&#xff08;一&#xff09;物理层面&#xff08;1&#xff09;网线&#xff08;2&#xff09;网线的制作&#xff08;3&#xff09;网卡接口&#xff08;4&#xff09;光纤&#xff08;5&#xff0…

【图像配准】使用OpenCV进行多图配准拼接

本篇主要利用OpenCV自带的配准拼接函数Stitcher_create来实现多幅图像的配准拼接 代码参考自&#xff1a;https://github.com/samggggflynn/image-stitching-opencv 图像拼接创建步骤 通常来说&#xff0c;根据多个图像创建全景图的步骤为以下几步&#xff1a; 检测两张图像的…

【pytest】三、pytest之setup和teardown,及封装

一、引言&#xff1a; 1&#xff09;setup的作用&#xff1a;用来实现执行前的一些初始化操作(如&#xff1a;数据准备、连接设备、打开APP/浏览器、创建日志对象、创建数据库连接、创建接口的请求对象等操作)&#xff1b; 2&#xff09;teardown的作用&#xff1a;用来实现执…

C++学习/温习笔记:新型源码学编程(二)

写在前面 面向初学者撰写专栏&#xff0c;个人原创的学习C/C笔记&#xff08;干货&#xff09;编程练习所作源代码输出内容为中文&#xff0c;便于理解如有错误之处请各位读者指正请读者评论回复、参与投票&#xff0c;反馈给作者&#xff0c;我会获得持续更新笔记干货的动力。…

放弃内卷,创造新市场

在疫情&#xff0c;突然爆发的俄乌冲突&#xff0c;还有全球的物价上涨情况等社会问题逐渐变得尖锐的动荡中&#xff0c;让原本就不好的经济形势进入了寒冬期&#xff0c;各个行业也陆续进入了寒冬期&#xff0c;纷纷进入了内卷模式&#xff0c;尤其是对于跨境电商行业来说&…

linux 部署jmeter报错处理

一、linux 安装jdk Java Downloads | Oracle 二、 linux上传jmeter 2.1 上传jmeter jmeter 下载地址&#xff1a; Apache JMeter - Download Apache JMeter 注意&#xff1a; 我先在我本地调试脚本&#xff08;mac环境&#xff09;&#xff0c;调试完成后&#xff0c;再在…

java基于ssm的饰品商城的设计与实现

管理员&#xff1b;管理员使用本系统涉到的功能主要有个人中心、用户管理、商品分类管理、商品信息管理、活动商品管理、知识普及管理、饰品圈、系统管理、订单管理等功能。 用户进入前台可以查看首页、商品信息、活动商品、知识普及、饰品圈、公告信息、个人中心、后台管理、购…

全国产!全志A40i+Logos FPGA核心板(4核ARM Cortex-A7)硬件说明

硬件资源 SOM-TLA40iF核心板板载ARM、FPGA、ROM、RAM、晶振、电源、LED等硬件资源,并通过B2B连接方式引出IO。核心板所有器件(包括B2B连接器)均采用国产工业级方案,国产化率100%。 图 1 核心板硬件框图

哪有什么互联网寒冬?献给java程序员的面试全攻略,修炼内功,强大自己才是王道!

最近很多人在讨论&#xff0c;学Java还有前途吗&#xff1f;转行Java有希望吗?Java还值得学吗&#xff1f;诸如此类的问题太多了&#xff0c;见怪不怪&#xff0c;不用担心太多&#xff0c;我们来仔细分析一下为什么会出现这种情况。 首先&#xff0c;互联网行业近几年确实很…

scrapy构造并发送请求

scrapy数据建模与请求 学习目标&#xff1a; 应用 在scrapy项目中进行建模应用 构造Request对象&#xff0c;并发送请求应用 利用meta参数在不同的解析函数中传递数据 1. 数据建模 通常在做项目的过程中&#xff0c;在items.py中进行数据建模 1.1 为什么建模 定义item即提前…

【Linux】操作系统(Operator System)

目录 一、操作系统是什么 1、操作系统概念 2、操作系统的定位 3、设计OS的目的 4、操作系统的重要性 二、 理解操作系统 三、系统调用 系统调用和库函数的关系 一、操作系统是什么 1、操作系统概念 任何计算机系统都包含一个基本的程序集合&#xff0c;称为操作系统…

【网络设备排障怎么破?这五个命令申请出战!】

1.网络排错常用诊断工具介绍 主流网络设备产品提供了一套完整的命令集&#xff0c;可以用于监控网络互联环境的工作状况和解决基本的网络故障。主要包括以下命令&#xff1a; Ping命令 Traceroute命令 Show命令 Clear命令 Debug命令 1.1 Ping命令 1.原理&#xff1a; “ping”…

vue+element-ui项目搭建记录

一. 安装vue&#xff08;省略&#xff09; 二. 新建vue项目并启动 1. 命令行执行新建项目命令&#xff1a; vue init webpack TestElemntUI3 //TestElemntUI3为项目名称执行结果发现有问题&#xff1a; 2. 需要安装一个全局加载项&#xff0c;执行命令&#xff1a; npm i…

ARM+DSP!全志T113-i+玄铁HiFi4开发板硬件说明书(1)

前 言 本文档主要介绍开发板硬件接口资源以及设计注意事项等内容,测试板卡为全志T113-i+玄铁HiFi4开发板。由于篇幅问题,本篇文章共分为上下两集,点击账户可查看更多内容详情,开发问题欢迎留言,感谢关注。 T113-i处理器的IO电平标准一般为1.8V和3.3V,上拉电源一般不超过…

DSTL与PA咨询公司助力英国国防部发展“量子+军事”领域

DSTL的量子实验室&#xff08;图片来源&#xff1a;网络&#xff09;如何充分利用国防和安全领域的资源并确保人员安全&#xff1f;快速准确的决策至关重要。人类虽然是决策过程的重要参与者&#xff0c;但是快节奏的决策过程对人类来说变得越来越复杂。国防科学技术实验室&…

18种可以提高接口性能的方案

之前工作中&#xff0c;遇到一个504超时问题。原因是因为接口耗时过长&#xff0c;超过nginx配置的10秒。然后 真枪实弹搞了一次接口性能优化&#xff0c;最后接口从11.3s降为170ms。本文将跟小伙伴们分享接口优化的一些通用方案。 1. 批量思想&#xff1a;批量操作数据库 优化…

分布式系统架构解决方案—Dubbo

文章目录什么是分布式系统&#xff1f;系统架构Dubbo简介RPC的概念dubbo节点角色dubbo节点调用关系Dubbo 安装管理端Dubbo-监控统计中心什么是分布式系统&#xff1f; “分布式系统是若干独立计算机的集合&#xff0c;这些计算机对于用户来说就像单个相关系统”分布式系统&…

第一次创业,注册什么类型的公司更好?

前言 几乎每一个打工者都有一颗当老板的心&#xff0c;大喊一声&#xff1a;"大丈夫生居天地间,岂能郁郁久居人下"&#xff0c;于是一拍桌子就辞职创业&#xff0c;现实往往都是潦草收场&#xff0c;看下面一段统计数据&#xff1a; 中国小微企业平均存活周期4.13年&…

2023牛客寒假算法基础集训营4(10/13)

清楚姐姐学信息论数学&#xff0c;只需要求x的y次方和y的x次方那个大选哪个&#xff0c;除了2和3时是3多&#xff0c;其他情况都是数越小能代表的数越多AC代码&#xff1a;#include <bits/stdc.h> using namespace std; using LL long long; int main() {ios::sync_with…