大文件上传

文章探讨了处理大文件上传时遇到的问题,如内存溢出、网络中断和传输效率低下,提出了解决方案如分块、多线程、异步处理、断点续传、压缩和MD5校验。重点介绍了秒传功能的实现原理,以及如何利用Redis存储、内存映射和消息队列进行优化。
摘要由CSDN通过智能技术生成

项目背景 

 文件过大——》内存溢出、系统崩溃;

网络影响——》传输中断,重新上传;

传输时间长——》用户不知道传输进度,影响用户体验;

长传下载速度慢——》影响用户体验;

分块;

多线程、异步;

进度;

断点续传;

压缩;秒传;消息队列

  • 文件分片: 为了处理大文件上传,通常需要将文件分割成较小的片段或块。这样可以减少每个请求的数据量,提高上传的性能和可靠性。
  • 断点续传: 断点续传是指在上传过程中,如果上传中断或失败,可以从中断的地方继续上传,而不需要重新上传整个文件。后端需要记录上传进度,并能够根据客户端请求进行相应的处理。
  • 存储管理: 后端需要选择适当的方式来管理上传的文件,可以将文件存储在本地文件系统、云存储服务如AWS S3、阿里天0SS或其他分布式文件系统中
  • 并发处理:处理大文件上传可能会涉及到多个并发请求,后端需要考虑并发处理的策略,例如使用线程池或异步任务来外理上传造求,以充分利里服务器资源
  • 安全性:对于大文件上传,安全性也是重要的考虑因素。后端需要对上传的文件进行验证、权限控制和防御措施,以防止恶意文件或攻击。
  • 进度跟踪:为了提供用户友好的上传体验,后端可以通过记录和更新上传进度,向前端提供上传进度信         ,这可以通过定时轮询、Websocket或服务器推送等方式实现。
  • 文件秒传:

md5

MD5 在文件上传与下载中的应用主要是用于校验文件的完整性

  • 在文件上传时,服务器通常会计算文件的 MD5 哈希值,并将其记录在数据库或其他存储介质中。
  • 当用户需要下载文件时,客户端也会计算文件的 MD5 哈希值,并将其与服务器记录的哈希值进行对比,以判断文件是否被篡改过或传输过程中是否发生了错误
  • 也用于秒传,后文介绍

文件上传: 

  • 1. 客户端将文件分成若干个块,并分别计算每个块的 MD5 哈希值 
  • 2. 客户端将文件块和对应的MD5哈希值上传到服务器。
  • 3. 服务器接收到文件块后,计算每个块的哈希值,并与客户端提供的哈希值进行比对 。如果有任何一个块的哈希值不匹配,服务器将拒绝文件上传,并提示用户重新上传。
  • 4. 如果所有块的哈希值都匹配,服务器将把块合并成完整的文件,并计算文件的 MD5 哈希值。服务器将文件的哈希值记录在数据库或其他存储介质中。                

文件下载:         

  • 1. 客户端向服务器请求下载文件,并附带文件的 MD5 哈希值。            
  • 2. 服务器从数据库或其他存储介质中获取文件的哈希值,并将其返回给客户端。     
  • 3. 客户端下载文件,并计算文件的 MD5 哈希值。                                        
  • 4. 客户端将计算出的哈希值与服务器返回的哈希值进行比较。如果两者相同,说明文件完整无损;否则,说明文件可能已被篡改或传输过程中发生了错误。             

在实际应用中,为了提高安全性,可能需要对 MD5 哈希值进行加盐、多次迭代等处理,以防止恶意攻击。同时,为了提高上传和下载的效率,可以使用分块传输、断点续传等技术,减少因网络不稳定等原因导致的传输失败。                                                               

秒传功能

指在文件上传过程中,如果发现服务器已存在相同的文件,则可以通过校验文件的MD5值来实现快速上传,跳过重复上传的过程,从而实现秒传的效果。 

实现秒传功能的一种常见思路是使用文件的MD5值进行比对

  • 1. 客户端将待上传的文件进行MD5计算,并将计算结果发送给后端。
  • 2. 后端接收到MD5值后,先检查服务器上是否已经存在该MD5对应的文件。
  • 3. 如果服务器上存在该文件,则直接返回已存在的文件路径给客户端,完成秒传。
  • 4. 如果服务器上不存在该文件,则要求客户端上传整个文件。
  • 5. 客户端进行文件上传操作,后端接收到文件后进行MD5计算,并与客户端发送的MD5进行比对。
  • 6. 如果两者一致,则表示文件上传成功,并将文件保存在服务器上,同时将文件路径返回给客户端。
  • 7. 如果两者不一致,则表示文件上传失败,客户端需要重新上传。

在实际实现中,可以使用Java的MessageDigest类来计算文件的MD5值,使用文件系统或数据库记录文件的MD5值和路径信息。另外,为了提高效率,可以考虑使用分布式文件系统或对象存储服务来存储文件,以便实现文件的高可用和快速访问。

秒传思路

  • 当客户端上传文件时,同时进行MD5值的计算并将计算结果发送给后端;后端接收到MD5值,在服务器中查找是否存在该md5对应的文件(查询该MD5是否已经存在用redis来存储数据,用文件MD5值来作key,value是文件存储的地址),如果服务器存在该文件,则直接返回已存在的文件路径,完成秒传。
  • 否则客户端进行文件分片上传。分片计算每块文件的md5哈希值,并将文件块和对应md5上传到服务器,服务器再次进行MD5的计算,如果不一致,终止上传。如果所有块的哈希值都匹配,服务器将把块合并成完整的文件,并计算文件的 MD5 哈希值。服务器将文件的哈希值记录在数据库或其他存储介质中。

                                                                                

使用redis来存储MD5值和文件路径的优势:

  • 快速查询,读写性能高;
  • 内存存储,减少对磁盘的IO操作;(磁盘IO是指计算机系统与硬盘之间进行的数据输入与输出,涉及物理层面的运动,比较慢延迟较高)
  • 高并发支持,Redis是单线程的,采用了事件驱动的机制,能够支持高并发的访问。对于秒传功能这种可能会有大量并发请求的场景,Redis能够提供较好的性能表现。http://t.csdnimg.cn/pKe0S
  • 数据结构简单,更加灵活操作;

分片上传 

分块上传 ——>避免将整个文件加载到内存——>可以使用对流的处理 ,

//RandomAccessFile更适合小文件,支持多线程,适合灵活随机读取文件的各个部分。

//MapperdByteBuffer:适合大文件,将文件映射到内存来处理文件,直接访问内存,避免频繁对磁盘进行io操作,

http://t.csdnimg.cn/OGoP1

    public void uploadFileRandomAccessFile(MultipartFileParam param) throws IOException {
        String fileName = param.getName();
        String tempDirPath = finalDirPath + param.getMd5();
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(tempDirPath);
        File tmpFile = new File(tempDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        //随机访问文件的对象
        RandomAccessFile accessTmpFile = new RandomAccessFile(tmpFile, "rw");
        long offset = CHUNK_SIZE * param.getChunk();//param.getChunk()当前上传的分片号,
        // 定位到该分片的偏移量
        accessTmpFile.seek(offset);
        // 写入该分片数据
        accessTmpFile.write(param.getFile().getBytes());
        // 释放
        accessTmpFile.close();


        boolean isOk = checkAndSetUploadProgress(param, tempDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }

    }

根据传入的参数,创建一个唯一的临时文件夹和临时文件,用于临时存放文件内容。偏移量=每个分片大小*文件的分片号,使用RandomAccess类中的方法定位到文件分片的偏移量位置,并写入分片数据,释放。最后检测分片文件上传的进度。

   @Override
    public void uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException {
        String fileName = param.getName();
        String uploadDirPath = finalDirPath + param.getMd5();
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(uploadDirPath);
        File tmpFile = new File(uploadDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
        FileChannel fileChannel = tempRaf.getChannel();//文件通道可以用于进行文件的读写操作。

        // 写入该分片数据
        long offset = CHUNK_SIZE * param.getChunk();
        byte[] fileData = param.getFile().getBytes();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        mappedByteBuffer.put(fileData);
        // 释放
        FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();

        boolean isOk = checkAndSetUploadProgress(param, uploadDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }
    }

多线程处理版本 

@Override
public void uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException {
    String fileName = param.getName();
    String uploadDirPath = finalDirPath + param.getMd5();
    String tempFileName = fileName + "_tmp";
    File tmpDir = new File(uploadDirPath);
    File tmpFile = new File(uploadDirPath, tempFileName);
    if (!tmpDir.exists()) {
        tmpDir.mkdirs();
    }

    RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
    FileChannel fileChannel = tempRaf.getChannel();//文件通道可以用于进行文件的读写操作。

    // 写入该分片数据
    long offset = CHUNK_SIZE * param.getChunk();
    byte[] fileData = param.getFile().getBytes();

    int parallelism = 4; // 假设同时处理4个分片,根据实际情况调整
    CountDownLatch countDownLatch = new CountDownLatch(parallelism);
    ExecutorService executorService = Executors.newFixedThreadPool(parallelism);

    for (int i = 0; i < parallelism; i++) {
        int startOffset = i * fileData.length / parallelism;
        int endOffset = (i + 1) * fileData.length / parallelism;
        byte[] subData = Arrays.copyOfRange(fileData, startOffset, endOffset);

        executorService.submit(() -> {
            try {
                MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset + startOffset, subData.length);
                mappedByteBuffer.put(subData);
                FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
            } finally {
                countDownLatch.countDown();
            }
        });
    }

    try {
        countDownLatch.await(); // 等待所有线程完成
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        executorService.shutdown();
    }

    fileChannel.close();

    boolean isOk = checkAndSetUploadProgress(param, uploadDirPath);
    if (isOk) {
        boolean flag = renameFile(tmpFile, fileName);
        System.out.println("upload complete !!" + flag + " name=" + fileName);
    }
}

流式处理 

相比将整个文件加载到内存中进行处理,流式处理可以边读取边处理数据,从而减少对内存的需求,并且能够处理更大的文件。

BufferedReader是Java中的一个输入流,它提供了缓冲读取字符的功能,可以逐行读取文本数据。处理大型文件时减少IO操作次数,提高读取效率。

(*缓冲区对IO系统调用的影响:从用户态切换到内核态就涉及系统调用,eg:让磁盘写1000次,每次写入1个字节还是一次性让磁盘写1000字节,我们更愿意选择后者。当向磁盘写入大量数据时,将数据存储在缓冲区中并一次性地将整个缓冲区写入磁盘,通常比每次写入一个字节要高效得多。这是因为单次写入大块数据会减少对磁盘的访问次数,而磁盘IO操作往往是相对较慢的。此外,大块数据写入也有利于提高磁盘的吞吐量,从而减少了系统调用的开销。另外,缓冲区还可以在内存中暂时保存数据,以便应用程序可以在更方便的时间点进行读取或写入操作。这种机制可以减少频繁的IO操作,提高IO效率。

总结,使用缓冲区,可以有效地减少IO系统调用的次数,降低了用户态和内核态之间的切换开销,并提高了系统的整体IO性能。)

详情参考 http://t.csdnimg.cn/g6M2j

内存映射文件——减少系统IO调用次数

多线程+异步io加速上传和下载的过程 ——> 线程池的创建              

利用多线程可以同时处理多个文件或文件的多个部分比如每个块对应一个线程,从而提高上传和下载的速度。

                  

给客户端进度上传和下载的反馈

 特大文件——>先压缩后上传或者下载

消息队列

削峰填谷,流量控制:减小对网络以及服务器的压力,设置缓冲区大小,限制并发数

异步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sqyaa.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值