Skip to content

分片,分点,秒传

更新: 8/6/2025 字数: 0 字 时长: 0 分钟

本文参考

原始的上传文件的方式其实是一个连续的过程,前端一直在发送,后端一直在接受,中间不存在暂停或是断网后的重连一类的过程,这对于生成是很难忍受的,因此我们才会需要分片,分点上传。

分片上传

在前端要求前端将一个文件拆分成多个小块,然后发送多次请求到后端,后端依次接受这些小片,然后再将其整合为原文件。

问题

  • 如何将接受到的多个文件整合成一个文件?
  • 如何确定哪些文件属于一个文件?

断点续传

当出现网络问题或用户主动中断文件上传的过程时,我们会希望恢复到上传状态时文件可以从上次中断的地方继续上传,而不是重新上传,这就是断点上传。

断点上传本身是分片上传的一种延申,因此实现分片上传逻辑的代码都可以很容易的接入断点上传。

分片/分点整体过程

  1. 前端将文件按照一定的比例分割,每次上传固定的比例(一个分片),给文件做上序号
  2. 后端将前端上传的分片放入一个缓存目录
  3. 等待前端将全部文件上传完毕后,发送一个合并文件的请求。
  4. 后端使用RandomAccessFile进入多线程读取所有分片,一个线程一个分片
  5. 如果出现了断网或暂停,则删除最后一个分片(故障分片)
  6. 前端从被删除的那个分片开始重新上传

分片/分点实现步骤

  1. 前端将固定大小的文件进行分片,然后请求后端带上分片大小和序号
  2. 服务端创建conf文件用来记录分片的位置,conf文件长度为总分片数,每上传一个分片即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127
  3. 服务器按照分片的序号和每片分块的大小算出开始位置,然后读取相应文件段,写入数据

写入文件要使用的类

对于写入文件要使用的类有以下两个

  • RandomAccessFile
  • MappedByteBuffer

RandomAccessFile(随机访问类)

这里的随机指的是可以跳转到一个文件的任意一个部分去修改和读取,这个类的使用与我们在C语言中使用过的fopen很像,使用类似命令的方式去操作文件。

该类内部有个位置指示器,可以指向当前读写处的位置,每读取n个字节后,文件指示器将指向这n个字节后面的下一个字节处

java
//创建随机存储文件流,文件属性由参数File对象指定
RandomAccessFile(File file , String mode)

//创建随机存储文件流,文件名由参数name指定
RandomAccessFile(String name , String mode)

这里的mode就是用来填我们的读写模式

  • r: 只读,写会报IOException
  • rw: 读写,会自动创建不存在的文件,但此时并非真正的写入文件,而是先写入系统缓存,由操作系统决定何时去写入硬盘中。
  • rws: 同步读写,会在每次更新时将文件的内容元数据都同步更新到硬盘中去,s就是同步的英文synchronous
  • rwd: 同步读写,会在每次更新时文件内容同步更新到硬盘中去,而元数据(如文件修改时间)的更新则不要求同步写入,d指的是数据的英文data

ps:元数据指的是文件除了内容以外的一些附加属性,用于描述文件的一些性质,比如文件大小,文件修改时间等,操作系统会需要通过这些元数据来对文件进行浏览,判断是否备份等操作

MappedByteBuffer

普通的读写要使用BufferedReader,BufferedInputStream这种带缓冲的IO类来处理大文件,而对于一些更大到影响性能的文件我们可以选择使用NIO为我们提供的MappedByteBuffer。

MappedByteBuffer使用了虚拟内存技术来优化了对大文件的使用与读写。

一般来说我们的内存包含用户空间和系统空间,传统的IO操作中我们要使用一端数据需要先将数据拷贝到系统空间,再拷贝到用户空间。

这一过程中内核缓冲区到用户缓冲区的“复制”过程,被我们称之为“Buffer 拷贝”

但在现代IO中,会有人产生思考,能不能省略掉Buffer拷贝的这一步,单独使用一块内存空间,将数据从磁盘映射到内存,然后用户直接访问映射的物理内存不就好了吗。

其中这个被单独划分出的内存区域叫做虚拟地址空间,而不同空间到虚拟地址的映射称之为BufferMap,Java中通过MappedByteBufer来进行这种操作。

这里有两点需要注意:

  • 虚拟地址空间是实现这个映射的工具,它本身不存放数据,而是通过“映射”关系,让用户程序能够直接访问到物理内存中存放的、来自磁盘的数据。
  • 虚拟地址空间并不全是内存上的空间,有时会使用硬盘上的空间进行暂存,使用时通过交换将硬盘上暂存的数据读取到内存中

如果我们观察MappedByteBuffer的源码会发现他其实是一个抽象类,这也就导致我们不能直接通过new的方式来获取他的实例。

对此我们要使用先使用RandomAccessFile来获取来获取一个FileChannel,再通过FileChannel的map方法来获取MappedByteBuffer的实现类

java
public void readWithMap() throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/xxx"), "r"))
        {
            //get Channel
            FileChannel fileChannel = file.getChannel();
            //get mappedByteBuffer from fileChannel
            MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
            // check buffer
            log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一个提醒而不是guarantee
            log.info("capacity {}",buffer.capacity());
            //read the buffer
            for (int i = 0; i < buffer.limit(); i++)
            {
                log.info("get {}", buffer.get());
            }
        }
    }

MappedByteBuffer的实现类有DirectByteBuffer和DirectByteBufferR,分别表示普通的类和只读类。

java
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

这里的mode有以下几种

  • FileChannel.MapMode.READ_ONLY 表示只读模式
  • FileChannel.MapMode.READ_WRITE 表示读写模式
  • FileChannel.MapMode.PRIVATE 表示copy-on-write模式,这个模式和READ_ONLY有点相似,它的操作是先对原数据进行拷贝,然后可以在拷贝之后的Buffer中进行读写。但是这个写入并不会影响原数据。可以看做是数据的本地拷贝,所以叫做Private。

基本的MapMode就这三种了,其实除了基础的MapMode,还有两种扩展的MapMode:

  • ExtendedMapMode.READ_ONLY_SYNC 同步的读
  • ExtendedMapMode.READ_WRITE_SYNC 同步的读写

秒传

当我们向服务器上传文件时有时会遇到一种特别的情况,那就是一个相对较大的文件一瞬间就传好了,这显然是不可能的,那么究竟发生了什么呢?

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

秒传实现逻辑

利用redis的set方法来存放文件的上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。

如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中,key为上传文件md5加一个固定前缀,value为块号文件记录路径

本站访客数 人次      本站总访问量