minio分片上传
oss文件服务
一、前言
Minio是一个对象存储服务OSS(Object Storage Service)。是⼀种海量、安全、低成本、⾼可靠的云存储服务。本身的应用的并不复杂。
但是Minio的APi在对于大于5m的文件,自动采用了分片上传,它的分片上传我们无法得知上传的分片后的序号,也就是说,没上传一个分片,我们都需要自己去记录已上传分片的序号。这将导致一个文件一个文件分片5个,那么同样还需要调用5次后端接口去记录这5个分片的信息。这个无疑大大浪费了性能,且无法做到并发上传。
因此基于Minio的javaAPI,我们采用另一种方案去替代。
二、初步流程:
- 前端服务进行大文件分片处理,将分片信息传递给文件服务oss。
- oss通过redis和mysql检查分片信息是否已存在,若存在直接返回数据。
- 若不存在oss生成上传链接以及uploadID, 然后记录并返回所有分片的上传链接及uploadId。
- 前端服务直接请求Minio 服务器,并发上传分片。
- 所有分片上传完成后,使用uploadId 调用文件服务进行文件合并,oss同时更新上传状态。
三、具体实现
1 相关配置准备
1.1 数据库表设计
表名:file_record
字段名 | 类型 | 注释 |
---|---|---|
id | bigint | 主键id |
file_url | text | 上传分片的链接 |
file_name | varchar | 文件名 |
md5 | varchar | MD5 |
upload_id | varchar | 上传id |
is_uploaded | int | 是否已上传 |
total_chunks | int | 分片总块数 |
size | bigint | 文件大小(K) |
completed_parts | int | 已完成片数 |
created_at | datetime | 生成时间 |
updated_at | datetime | 更新时间 |
deleted_at | datetime | 删除时间(软删除) |
1.2 minio集成配置
linux上部署minio,docker下载minio镜像
docker run -d -p 10000:10000 -p 11000:11000 --name minio2 -v ~/var/local/environment/data:/data -e "MINIO_ROOT_USER=root" -e "MINIO_ROOT_PASSWORD=minio123456" minio/minio server /data --console-address ":11000" --address ":10000"
配置说明:这里选择开放两个端口10000和11000。
–console-address ":11000"是将控制台的端口11000暴露
–address ":10000"是我们集成到项目中占用的端口。
nacos上配置oss-dev.yml
minio: url: http://192.168.137.129:10000 accessKey: root secretKey: minio123456 bucketName: test-bucket
minio控制台(可管理文件和bucket):
项目依赖引入,这里使用的是当前的最新版8.3.5
<dependency> <groupId>io.miniogroupId> <artifactId>minioartifactId> <version>8.3.5version>dependency>
2 后端实现
2.1 MinioProperty
MinioProperty类获取minio配置
import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;@Data@Configuration@ConfigurationProperties(prefix = "minio")public class MinioProperty { public String url; public String accessKey; public String secretKey; public String bucketName; public Boolean secure = false;}
2.2 MyMinioClient
MyMinioClient类继承MinioClient来暴露出父类方法供扩展使用。
import com.google.common.collect.Multimap;import io.minio.CreateMultipartUploadResponse;import io.minio.ListPartsResponse;import io.minio.MinioClient;import io.minio.ObjectWriteResponse;import io.minio.errors.*;import io.minio.messages.Part;import java.io.IOException;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;public class MyMinioClient extends MinioClient { protected MyMinioClient(MinioClient client) { super(client); } @Override public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams); } @Override public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams); } public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams); } public CreateMultipartUploadResponse uploadId(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams); } @SneakyThrows public String getPresignedObjectUrl(String bucketName, String filePath, Map<String, String> queryParams) { return super.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucketName) .object(filePath) .expiry(1, TimeUnit.DAYS) .extraQueryParams(queryParams) .build()); }}
2.3 MinioConfig
MinioConfig将MyMinioClient交给spring容器管理方便调用
import io.minio.MinioClient;import lombok.SneakyThrows;import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class MinoConfig { @Bean @SneakyThrows @ConditionalOnMissingBean(MyMinioClient.class) public MyMinioClientminioClient(MinioProperty minioProperty) { MinioClient minioClient = MinioClient. builder() .endpoint(minioProperty.getUrl()) .credentials(minioProperty.getAccessKey(), minioProperty.getSecretKey()) .build(); return new MyMinioClient(minioClient); }}
2.4 核心接口实现
2.4.1 createMultipartUpload方法
返回分片上传需要的签名数据URL及 uploadId。
fileMultipartDTO 是分片传输实体,包括:文件名fileName、分片数chunkSize、所属文件夹名bucketName。
检查redis中是否存在,若存在直接返回对应数据;若redis中不存在检查数据库中是否存在,若存在直接返回对应数据。
若redis、数据库中都不存在,处理生成分片。
调用minioAPI获得uploadId和签名url
相关数据在redis中缓存,并保存到数据库。返回给前端
public Map<String, Object> createMultipartUpload(FileMultipartDTO fileMultipartDTO) { log.info("fileMultipartDTO:{}", fileMultipartDTO); String fileName = fileMultipartDTO.getFileName(); Integer chunkSize = fileMultipartDTO.getChunkSize(); String bucketName = fileMultipartDTO.getBucketName(); // 1. 根据文件名创建签名 // 2. 获取uploadId String contentType = "application/octet-stream"; HashMultimap<String, String> headers = HashMultimap.create(); headers.put("Content-Type", contentType); CreateMultipartUploadResponse response = minioClient.uploadId(bucketName, null, fileName, null, null); String uploadId = response.result().uploadId(); Map<String, Object> result = new HashMap<>(3, 1); String md5 = MD5Utils.encryptToMd5(fileName + chunkSize + bucketName); //查询是否已存在,若存在返回对应数据 Map<String, Object> checkResult; //检查redis中是否已存在 checkResult = checkIsExistInRedis(md5); if (checkResult != null && !checkResult.isEmpty()) { log.info(">>>>>>>>>>>>redis中存在"); return checkResult; } //检查数据库中是否已存在 checkResult = checkIsExistInDatabase(md5); if (checkResult != null && !checkResult.isEmpty()) { log.info(">>>>>>>>>>>>数据库中存在"); //redis中补充 checkResult.put("fileName", fileName); redisTemplate.opsForValue().set(RedisPrefixForKey.MINIO_KEY + md5, checkResult, 1, TimeUnit.HOURS); return checkResult; } //redis中和数据库中都没有,需要生成 result.put("uploadId", uploadId); // 3. 请求Minio 服务,获取每个分块带签名的上传URL Map<String, String> reqParams = new HashMap<>(3, 1); reqParams.put("uploadId", uploadId); List<String> uploadUrlList = new ArrayList<>(); // 4. 循环分块数 从1开始 for (int i = 1; i <= chunkSize; i++) { reqParams.put("partNumber", String.valueOf(i)); // 获取URL String uploadUrl = minioCilent.getPresignedObjectUrl(bucketName, fileName, reqParams); // 添加到集合 result.put("chunk_" + (i - 1), uploadUrl); uploadUrlList.add(uploadUrl); } log.info(">>>>分片数据入redis和数据库"); result.put("fileName", fileName); redisTemplate.opsForValue().set(RedisPrefixForKey.MINIO_KEY + md5, result, 8, TimeUnit.HOURS); //入库 String uploadUrls = JSON.toJSONString(uploadUrlList); FileRecord fileRecord = new FileRecord() .setFileName(fileName) .setFileUrl(uploadUrls) .setUploadId(uploadId) .setTotalChunks(chunkSize) .setMd5(md5) //上传完成后,接收到合并请求时再改以下参数 .setCompletedParts(0) .setSize(0) .setIsUploaded(0); fileRecordMapper.insert(fileRecord); return result;}
2.4.2 completeMultipartUpload方法
分片上传完后合并。
FileCompleteDTO是合并请求参数实体,包括:所属文件夹名bucketName、上传iduploadId、文件名objectName。
调用minioAPI获取该文件的所有分片partList。
将partList中part合并成parts。
调用minioAPI完成合并。
合并成功后修改数据库信息。
public Result completeMultipartUpload(FileCompleteDTO fileObject) { String bucketName = fileObject.getBucketName(); String uploadId = fileObject.getUploadId(); String objectName = fileObject.getObjectName(); int completedParts; int size = 0; try { Part[] parts = new Part[10000]; ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); List<Part> partList = partResult.result().partList(); completedParts = partList.size(); int partNumber = 1; log.info("总片数:================" + completedParts + "========"); for (Part part : partList) { parts[partNumber - 1] = new Part(partNumber, part.etag()); //etag就是分片信息 partNumber++; size += part.partSize(); } minioClient.completeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null); } catch (Exception e) { e.printStackTrace(); return Result.fail("合并失败:" + e.getMessage()); } //合并成功,入库 //通过uploadId修改对应的记录行 fileRecordMapper.updateStatusByUploadId(uploadId, completedParts, size); return Result.success();}
3 前端实现
此处对前端实现不做重点描述,项目提供了一个demo,后续可做参考修改。
四、效果展示
当前端选择完文件后:
数据库中新增数据:
redis中新增数据
当前端点击开始上传按钮后,前端服务直接请求Minio服务
完成上传后,数据库更新is_uploaded、Size、completed_parts
查看minio控制台,可以看到该文件。
来源地址:https://blog.csdn.net/qq_42852943/article/details/127734905
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341