一、文件微服务

1.1 文件微服务

文件上传功能,这类比较通用的业务,可以抽取为一个 文件微服务 。在这个文件微服务中需要有如下功能:

  • 处理文件上传。
  • 上传文件的查询。
  • 文件的删除。
  • 垃圾文件的处理。

在开发时需要将这些附件上传保存到存储服务器(阿里云、七牛云、FastDFS、MinIO等)并将这些附件记录、可访问的文件地址存储到数据库中;因为在产品详情页查看时需要使用到。要能够通过产品id查询到对应的附件。

说明了这个文件微服务需要记录下来,是由哪些业务上传过来的数据;而且附件的数量也比较庞大。所以需要有对应的数据库来记录业务与附件的关系。

1.2 文件上传技术分析

  • 文件存储:存储具体的文件;可以采用阿里云OSS存储。
  • 文件记录:直接记录上传的文件信息到数据库表,baoxian-file数据库是专门存放文件、附件的数据库;其中有一个附件的表 tab_file在这个表中存储附件的信息即可。

上传的资料很有可能比较大(700MB, 1G, 3G);那么将这些大文件发送到 后端微服务的时候,很有可能因为网络不稳定而上传失败或者要重试多次,影响体验。可以将这些资料大文件分割为多个1M大小的文件多次上传到后端微服务,提高上传的成功率。

简单文件上传时序图:

在这里插入图片描述

大文件分片上传时序图:
在这里插入图片描述

上传资料大文件的时候;需要对文件至少3次以上的处理;分别处理:分片初始化、n次分片上传、分片合并;所以也将对应3个上传接口。

二、对象存储

2.1 对象存储介绍

对象存储服务(Object Storage Service)是一种数据存储和管理模型,用于存储和组织非结构化数据(文件:文本、图片、音频、视频),通常以对象(Object)的形式存储数据。每个对象通常包括数据本身、元数据(描述数据的信息),以及一个唯一的标识符。总的来说;就是存文件的。也一般会称OSS为存储服务器。

  • 方式一:可以存储到服务器所在硬盘。
  • 方式二:可以自己搭建存储服务器;比如:MinIO、FastDFS都是可自行搭建的分布式文件存储服务器。
  • 方式三:可以使用第三方,自己不用搭;直接用就行。比如:阿里云OSS(https://oss.console.aliyun.com/bucket)、华为云OSS、七牛云等。

2.2 简单文件上传

使用Java SDK发起OSS请求,需要配置访问凭证。具体如下:

# 测试之前需要先设置系统环境变量;打开CMD 执行如下命令:
set OSS_ACCESS_KEY_ID=你自己在阿里云上的AccessKey
set OSS_ACCESS_KEY_SECRET=你自己在阿里云上的AccessKeySecret

# 永久生效
setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"

# 设置之后;可以通过如下命令查看是否设置成功. IDEA中要生效的话,可以重启IDEA
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%

引入maven依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>

简单上传代码:

package com.itheima.sfbx.file;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.File;

public class AliOSSUploadTest {

    /**
     * 简单上传
     */
    @Test
    public void testSimpleUpload() throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "baoxian-oss";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "upload/pic/logo.png";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
        String filePath= "E:\\pic\\logo.png";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);

        try {
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传文件。
            PutObjectResult result = ossClient.putObject(putObjectRequest);

            System.out.println(result);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

上传成功:
在这里插入图片描述

2.3 分片文件上传

大文件上传面临网络中断风险和传输时间过长的挑战。分片上传通过将文件分割为多个小分片并发传输,提供断点续传能力和传输性能优化,有效应对网络不稳定环境下的文件传输需求。

在上传大文件(超过5 GB)到OSS的过程中,如果出现网络中断、程序异常退出等问题导致文件上传失败,您需要使用分片上传的方式上传大文件。分片上传通过将待上传的大文件分成多个较小的碎片(Part),充分利用网络带宽和服务器资源并行上传多个Part,加快上传完成时间,并在Part上传完成之后调用CompleteMultipartUpload接口将这些Part组合成一个完整的Object。

使用场景:

  • 大文件加速上传。当文件大小超过5 GB时,使用分片上传可实现并行上传多个Part以加快上传速度。
  • 网络环境较差。网络环境较差时,建议使用分片上传。当出现上传失败的时候,您仅需重传失败的Part。
  • 暂停和恢复上传:分片上传任务没有过期时间。您可以随时暂停和恢复分片上传,直到完成或取消分片上传。
  • 文件大小不确定。可以在需要上传的文件大小还不确定的情况下开始上传,这种场景在视频监控等行业应用中比较常见。

分片文件上传流程说明:
在这里插入图片描述

  1. 将待上传文件按照一定大小进行分片。
  2. 使用InitiateMultipartUpload接口初始化一个分片上传任务。
  3. 使用UploadPart接口上传分片。
    文件切分成Part之后,文件顺序是通过上传过程中指定的partNumber来确定,所以您可以并发上传这些碎片。并发数并非越多越快,请结合自身网络状况和设备负载综合考虑。如果您希望终止上传任务,可调用AbortMultipartUpload接口,成功上传的Part会一并删除。
  4. 使用CompleteMultipartUpload接口将Part组合成一个Object。

使用限制:
单个文件的大小:不超过48.8TB。
分片数量:1~10,000个。
单个分片大小:最小值为100KB,最大值为5GB。最后一个分片的大小允许小于100KB。

分片上传代码:

/**
 * 分片上传
 */
@Test
public void testMultiPartUpload() throws Exception {
    // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
    String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
    EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
    // 填写Bucket名称,例如examplebucket。
    String bucketName = "baoxian-oss";
    // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
    String objectName = "upload/file/"+ UUID.randomUUID()+".pdf";
    // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
    // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
    String filePath= "E:\\file\\Java.pdf";

    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);

    try {
        //1、初始化上传
        InitiateMultipartUploadRequest initiateMultipartUploadRequest = new InitiateMultipartUploadRequest(bucketName, objectName);
        InitiateMultipartUploadResult initiateMultipartUploadResult = ossClient.initiateMultipartUpload(initiateMultipartUploadRequest);
        String uploadId = initiateMultipartUploadResult.getUploadId();
        System.out.println("分片上传初始化完成;uploadId:" + uploadId);
        //2、分片上传文件
        //2.1、读取要上传的文件
        File file = new File(filePath);
        //2.2、计算要分片上传的次数;每个分片大小设置为1M,计算要上传多少次
        //单个分片文件大小;1MB
        long partSize = 1024 * 1024;
        //文件总长度
        long fileLength = file.length();
        //分为几个分片
        int partCount  = (int) (fileLength / partSize);
        if (fileLength % partSize != 0) {
            partCount++;
        }
        System.out.println("总共分为:" + partCount + "个分片");
        //2.3、循环上传分片
        //记录每次每个分片上传之后的eTag
        List<PartETag> partETags = new ArrayList<PartETag>();
        for (int i = 0; i < partCount; i++) {
            //当前分片的文件起始位置
            long startPos = i * partSize;

            //创建分片请求对象
            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            //桶名称
            uploadPartRequest.setBucketName(bucketName);
            //上传的文件名
            uploadPartRequest.setKey(objectName);
            uploadPartRequest.setUploadId(uploadId);
            //分片号
            uploadPartRequest.setPartNumber(i + 1);
            //设置当前分片文件内容
            FileInputStream fileInputStream = new FileInputStream(file);
            fileInputStream.skip(startPos);
            uploadPartRequest.setInputStream(fileInputStream);

            //当前这个分片的大小;但是最后一个分片大小可能是不到1M的;所以需要处理
            long currentPartSize = (i+1==partCount)?(fileLength-startPos):partSize;
            uploadPartRequest.setPartSize(currentPartSize);

            //上传分片
            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);

            System.out.println("Part#" + uploadPartRequest.getPartNumber() + " ETag:" + uploadPartResult.getETag());
            //记录 etag
            partETags.add(uploadPartResult.getPartETag());
        }

        //3、完成分片上传;合并(阿里云端合并)
        CompleteMultipartUploadRequest completeMultipartUploadRequest
                = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);

        ossClient.completeMultipartUpload(completeMultipartUploadRequest);

        System.out.println("分片上传完成!");
    } catch (OSSException oe) {
        System.out.println("Caught an OSSException, which means your request made it to OSS, "
                + "but was rejected with an error response for some reason.");
        System.out.println("Error Message:" + oe.getErrorMessage());
        System.out.println("Error Code:" + oe.getErrorCode());
        System.out.println("Request ID:" + oe.getRequestId());
        System.out.println("Host ID:" + oe.getHostId());
    } catch (ClientException ce) {
        System.out.println("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network.");
        System.out.println("Error Message:" + ce.getMessage());
    } finally {
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }
}

上传成功:
在这里插入图片描述
在这里插入图片描述

posted on 2026-02-03 16:05  努力--坚持  阅读(2)  评论(0)    收藏  举报