Springboot 文件传输优化

        Spring Boot通过MultipartFile接口简化了文件上传的处理。在Spring MVC中,你可以使用@RequestParam注解与MultipartFile一起工作来接收上传的文件。Spring Boot默认配置了文件上传的大小限制,但是你可以通过application.properties进行自定义配置。

# 单个文件大小限制
spring.servlet.multipart.max-file-size=100MB

# 请求中所有文件的总大小限制
spring.servlet.multipart.max-request-size=150MB

1、Java IO vs NIO

        Java IO是针对流式数据设计的,每次只能操作一个字节的数据。这让它在处理大文件或进行频繁的文件操作时,性能会相对较低。相反,Java NIO设计用来处理块或缓冲区的数据,一个缓冲区可以包含多个字节的数据,因此在处理大文件或频繁的文件操作时,性能可能更好。

2、文件传输方法性能对比

a、Spring 文件上传提供的方法 MultipartFile#transferTo()

@PostMapping("/upload")
public void upload(@RequestParam("file") MultipartFile file) {
    File dest = new File("path/to/destination");
    file.transferTo(dest.toPath());
}

方法内部使用了java.nio.file.Files#copy(java.io.InputStream, java.nio.file.Path, java.nio.file.CopyOption...)来实现文件的读写操作,这是NIO API的一部分。然而,这种方式仍然是在IO级别进行操作,并没有使用到NIO中的通道(Channel)和缓冲区(Buffer)机制,因此性能表现并不优秀。

b、FileChannel#transferTo

        文件通道(FileChannel)是一种可以从文件中读取、写入的通道。一般来说,文件通道的性能比Java IO的流更好。transferTo()方法的存在就是为了提高性能。 

@PostMapping("/upload")
public void uploadNio(@RequestParam("file") MultipartFile file) {
    try (FileOutputStream fos = new FileOutputStream("path/to/destination");
         FileChannel outChannel = fos.getChannel()) {
        FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();
        inChannel.transferTo(0, inChannel.size(), outChannel);
    }
}

这个方法使用了Java NIO库中的FileChannel,并且使用到了操作系统的“零拷贝”特性。“零拷贝”可以减少用户空间和内核空间之间的数据拷贝,因此在处理大文件时,它的性能会比org.springframework.web.multipart.MultipartFile#transferTo(java.nio.file.Path)更好。

c、完整代码及耗时对比如下

@RequestMapping(value = "/upload", method = RequestMethod.POST,consumes = "multipart/form-data")
    public CommResp handleFileUpload(@RequestParam("file") MultipartFile file,@RequestParam String erp) {
        Preconditions.checkArgument(StringUtils.isNotBlank(erp), "上传人erp不存在");
        Preconditions.checkArgument(file != null && !file.isEmpty(), "文件不能为空");
        //获取原文件名
        String originalFilename = file.getOriginalFilename();
        Preconditions.checkArgument(StringUtils.isNotBlank(originalFilename),"文件名称不能为空");
        //获取文件后缀
        String suffixName = originalFilename.substring(originalFilename.lastIndexOf("."));
        // 文件重命名
        String storagePath = UPLOAD_PATH + "/" + erp + "_" + System.currentTimeMillis() + suffixName;
        new Thread(() -> {
            // 实现方式1
            long startTime = System.currentTimeMillis();
            try(//创建文件流
                FileInputStream fis = (FileInputStream)file.getInputStream();
                FileOutputStream fos = new FileOutputStream(UPLOAD_PATH + "/" + erp + "_1_" + System.currentTimeMillis() + suffixName);
                //创建通道(通道间传输)
                FileChannel inChannel = fis.getChannel();
                FileChannel outChannel = fos.getChannel()){
                //使用零拷贝上传
                inChannel.transferTo(0,inChannel.size(),outChannel);
            }catch (IOException e){
                log.error("文件上传失败",e);
            }
            log.info("elapsed time1: {}", System.currentTimeMillis() - startTime);
        }).start();

        // 实现方式2
        long startTime = System.currentTimeMillis();
        try{
            Path targetPath = Paths.get(storagePath);
            file.transferTo(targetPath);
        }catch (IOException e){
            log.error("文件上传失败",e);
            return CommResp.failure("文件上传失败");
        }
        log.info("elapsed time2: {}", System.currentTimeMillis() - startTime);
        return CommResp.success(new UploadResultBO(originalFilename.replace(suffixName, ""),storagePath));
    }

PublishController        :  elapsed time1: 402
PublishController        :  elapsed time2: 1178

d、进一步优化传输性能思路

  • 多线程分块传输

        将大文件分割成多个部分,为每个部分分配一个单独的线程进行读写操作。这样可以充分利用多核CPU的计算能力,提高数据处理的并行度。

代码实现:

@PostMapping("/upload")
    public void uploadNioMultiThread(@RequestParam("file") MultipartFile file) {
        try (RandomAccessFile raf = new RandomAccessFile("path/to/destination", "rw");
             FileChannel outChannel = raf.getChannel()) {
            FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();
            long size = inChannel.size();
            long blockSize = size / THREAD_NUM;
            CountDownLatch latch = new CountDownLatch(THREAD_NUM);
            for (int i = 0; i < THREAD_NUM; i++) {
                long start = i * blockSize;
                long end = (i == THREAD_NUM - 1) ? size : start + blockSize;
                executorService.submit(() -> {
                    try (FileChannel channel = (FileChannel) Channels.newChannel(file.getInputStream())) {
                        channel.position(start);
                        outChannel.position(start);
                        channel.transferTo(start, end - start, outChannel);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                });
            }
            latch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 使用内存映射文件

        NIO提供了内存映射文件的支持,可以通过映射到内存的方式来直接操作文件,省去了内核空间和用户空间之间数据复制的开销。

代码实现:

/**
     * 1、开启输入文件Channel
     * 2、计算文件大小
     * 3、循环上传文件内容,每次处理10mb大小的数据块
     * 4、使用FileChannel.MapMode.READ_ONLY模式,以只读的方式,将输入文件的内容映射到内存中
     * 5、使用内存映射,将生成的ByteBuffer写入到输出文件Channel中
     * 6、修改当前位置,以便处理下一个数据块
     * @param file
     */
    @PostMapping("/upload")
    public void uploadFileWithMemoryMapped(@RequestParam("file") MultipartFile file) {
        try (RandomAccessFile raf = new RandomAccessFile("path/to/destination", "rw");
             FileChannel outChannel = raf.getChannel()) {

            FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();
            long fileSize = inChannel.size();
            long pos = 0L;

            /* Map the file into memory, and upload it by parts. */
            while (pos < fileSize) {
                long limit = Math.min(10485760L, fileSize - pos);  // map 10MB at a time
                MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, pos, limit);
                outChannel.write(inMappedBuf);  // This writes data to the file
                pos += limit;
            }
        } catch (IOException e) {
            // handle exception
        }
    }

在处理大型文件传输时,可以考虑同时实现内存映射文件和多线程分片传输,以提高大型文件处理的程序性能。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值