如果做车站车次查询的网站需要什么消息信息,wordpress 上传图片,青岛新公司网站建设推广,动漫设计培训学院引言
文件存储已成为一个做任何应用都不可回避的需求。传统的单机文件存储方案在面对大规模数据和高并发访问时往往力不从心#xff0c;而分布式文件存储系统则提供了更好的解决方案。本篇文章我将基于Spring Boot 3 为大家讲解如何基于MinIO来实现分布式文件存储。
分布式存…引言
文件存储已成为一个做任何应用都不可回避的需求。传统的单机文件存储方案在面对大规模数据和高并发访问时往往力不从心而分布式文件存储系统则提供了更好的解决方案。本篇文章我将基于Spring Boot 3 为大家讲解如何基于MinIO来实现分布式文件存储。
分布式存储的出现
在探讨核心内容之前我们不妨先回顾分布式存储技术是如何伴随系统架构演变发展的。在单体架构早期文件直接存储于应用服务器中这种方式简单直接存取便捷。然而随着业务规模扩大和用户量激增系统架构逐步向分布式或微服务方向演进。此时若仍将文件存储在应用服务器中在负载均衡机制下可能导致文件访问异常 —— 用户上传的文件可能因路由到其他服务节点而无法访问。 面对这个挑战我们可以借鉴分层解决的架构思想将文件存储从应用服务中剥离集中在独立的存储服务中统一管理。这便是分布式文件存储系统的雏形。
技术选型
在了解了分布式存储的演进背景后让我们来梳理当前主流的分布式存储解决方案。
其他
FastDFS - 架构老旧社区活跃度低文档资料匮乏Ambry - 过度依赖 LinkedIn 技术栈通用性不足MooseFS - 部署配置繁琐运维门槛高MogileFS - 性能一般扩展性受限LeoFS - 更新维护缓慢生态系统不完善openstack - 架构复杂重量级不适合轻量级应用TFS - 主要服务于阿里内部外部支持有限ceph - 学习曲线陡峭配置调优复杂GlusterFS - 架构复杂问题定位困难OSS - 商业收费服务成本随数据量增长
✨MinIO
MinIO 是一款轻量级的分布式对象存储系统完全兼容 Amazon S3 云存储服务接口。其部署维护简单性能卓越成为我们的首选方案。
MinIO安装
MinIO 提供了多种部署方式包括单机部署和分布式部署。本文主要关注 Spring Boot 与 MinIO 的整合实践因此我们选择使用DockerPs没安装Docker的同学速速去安装或者用别的方式只要本地部署的能跑就行进行快速部署。
首先通过命令拉取镜像。
docker pull minio/minio接着在 本地创建一个存储文件的映射目录 D:\minio\dataPs我当前演示的环境是win系统大家根据自己的操作系统建个目录就行使用以下命令启动 MinIO 补充一个小细节MinIO 的安全限制要求用户名长度至少需要 3 个字符密码长度至少需要 8 个字符。 docker run -d --name minio -p 9000:9000 -p 9001:9001 -v D:\minio\data:/data -e MINIO_ROOT_USERroot -e MINIO_ROOT_PASSWORD12345678 minio/minio server /data --console-address :9001 --address :9000参数说明
-d: 后台运行容器--name: 容器名称-p: 端口映射9000用于API访问9001用于控制台访问-v: 目录映射将本地目录映射到容器的 /data-e: 环境变量设置管理员账号和密码--console-address: 指定控制台端口--restartalways: 容器自动重启策略--address :9000: 显式指定 API 端口
运行成功后访问 http://localhost:9001使用执行命令中的凭据Ps大家在使用时可以修改为自己的用户名和密码登录
用户名root密码12345678 登录系统后界面会提示创建桶。熟悉云服务商OSS服务的读者对此概念应该不陌生。对初次接触的读者可以将桶理解为一个命名空间或文件夹您可以创建多个桶每个桶内还能包含多层级的文件夹和文件。
这里我演示下控制台如何建桶和上传文件方便大家理解文件在MinIO上的存储结构。 只需要输入名称就可以建好之后可以看到桶的使用状态。 点击它进入桶的内部这里大家需要关注一个设置- Access Policy默认是Private。这个设置需要根据业务的实际情况来如果你的业务是需要提供一些不需要鉴权的公共访问的文件就设为public反之就保持private。我这里把它修改为public。 然后点击右上角的上传按钮进入上传页可以向桶内上传文件。 上传成功后可以在桶内看到文件。 点击文件可查看详情支持预览、删除、分享等多种功能。这些操作较为直观安装后各位读者可自行体验。本文重点关注不在控制台的操作就不做过多赘述了。
这里再强调一点存储在桶里的文件通过API访问的端口和控制台是不一样的。如果你对这里感觉迷惑可以回看一下上面我贴上的docker运行命令里配置了两个端口-9000和9001。如果要通过API访问查看这个文件的话通过拼接地址/端口号/桶名/文件路径查看那么刚测试上传的文件的访问API就是http://localhost:9000/test/1.gif在浏览器地址栏输入后可以看到。 # Spring Boot整合MinIO
这部分对于新建项目就不赘述了直接说下我使用的 Spring boot 版本为3.2.3供大家参考。
1.引入依赖
在pom.xml引入minIO的依赖版本大家自己使用你当前最新的版本即可。
!-- minio --
dependencygroupIdio.minio/groupIdartifactIdminio/artifactIdversion${latest.version}/version
/dependency2.添加配置
在yml配置文件中配置连接信息。
# minIO配置
minio:endpoint: http://127.0.0.1:9000 # MinIO服务地址fileHost: http://127.0.0.1:9000 # 文件地址hostbucketName: wechat # 存储桶bucket名称accessKey: root # 用户名secretKey: 12345678 # 密码3.编写配置类
import com.pitayafruits.utils.MinIOUtils;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
Data
public class MinIOConfig {Value(${minio.endpoint})private String endpoint;Value(${minio.fileHost})private String fileHost;Value(${minio.bucketName})private String bucketName;Value(${minio.accessKey})private String accessKey;Value(${minio.secretKey})private String secretKey;Beanpublic MinIOUtils creatMinioClient() {return new MinIOUtils(endpoint, fileHost, bucketName, accessKey, secretKey);}
}4.引入工具类
这个工具类封装了MinIO的核心功能为您提供了很多开箱即用的功能。通过引入它可以轻松实现文件上传、下载等操作让大家将更多精力集中在业务开发上。
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;/*** MinIO工具类*/
Slf4j
public class MinIOUtils {private static MinioClient minioClient;private static String endpoint;private static String fileHost;private static String bucketName;private static String accessKey;private static String secretKey;private static final String SEPARATOR /;public MinIOUtils() {}public MinIOUtils(String endpoint, String fileHost, String bucketName, String accessKey, String secretKey) {MinIOUtils.endpoint endpoint;MinIOUtils.fileHost fileHost;MinIOUtils.bucketName bucketName;MinIOUtils.accessKey accessKey;MinIOUtils.secretKey secretKey;createMinioClient();}/*** 创建基于Java端的MinioClient*/public void createMinioClient() {try {if (null minioClient) {log.info(开始创建 MinioClient...);minioClient MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();createBucket(bucketName);log.info(创建完毕 MinioClient...);}} catch (Exception e) {log.error(MinIO服务器异常{}, e);}}/*** 获取上传文件前缀路径* return*/public static String getBasisUrl() {return endpoint SEPARATOR bucketName SEPARATOR;}/****************************** Operate Bucket Start ******************************//*** 启动SpringBoot容器的时候初始化Bucket* 如果没有Bucket则创建* throws Exception*/private static void createBucket(String bucketName) throws Exception {if (!bucketExists(bucketName)) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}}/*** 判断Bucket是否存在true存在false不存在* return* throws Exception*/public static boolean bucketExists(String bucketName) throws Exception {return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());}/*** 获得Bucket的策略* param bucketName* return* throws Exception*/public static String getBucketPolicy(String bucketName) throws Exception {String bucketPolicy minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());return bucketPolicy;}/*** 获得所有Bucket列表* return* throws Exception*/public static ListBucket getAllBuckets() throws Exception {return minioClient.listBuckets();}/*** 根据bucketName获取其相关信息* param bucketName* return* throws Exception*/public static OptionalBucket getBucket(String bucketName) throws Exception {return getAllBuckets().stream().filter(b - b.name().equals(bucketName)).findFirst();}/*** 根据bucketName删除Buckettrue删除成功 false删除失败文件或已不存在* param bucketName* throws Exception*/public static void removeBucket(String bucketName) throws Exception {minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}/****************************** Operate Bucket End ******************************//****************************** Operate Files Start ******************************//*** 判断文件是否存在* param bucketName 存储桶* param objectName 文件名* return*/public static boolean isObjectExist(String bucketName, String objectName) {boolean exist true;try {minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());} catch (Exception e) {exist false;}return exist;}/*** 判断文件夹是否存在* param bucketName 存储桶* param objectName 文件夹名称* return*/public static boolean isFolderExist(String bucketName, String objectName) {boolean exist false;try {IterableResultItem results minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());for (ResultItem result : results) {Item item result.get();if (item.isDir() objectName.equals(item.objectName())) {exist true;}}} catch (Exception e) {exist false;}return exist;}/*** 根据文件前置查询文件* param bucketName 存储桶* param prefix 前缀* param recursive 是否使用递归查询* return MinioItem 列表* throws Exception*/public static ListItem getAllObjectsByPrefix(String bucketName,String prefix,boolean recursive) throws Exception {ListItem list new ArrayList();IterableResultItem objectsIterator minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());if (objectsIterator ! null) {for (ResultItem o : objectsIterator) {Item item o.get();list.add(item);}}return list;}/*** 获取文件流* param bucketName 存储桶* param objectName 文件名* return 二进制流*/public static InputStream getObject(String bucketName, String objectName) throws Exception {return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** 断点下载* param bucketName 存储桶* param objectName 文件名称* param offset 起始字节的位置* param length 要读取的长度* return 二进制流*/public InputStream getObject(String bucketName, String objectName, long offset, long length)throws Exception {return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());}/*** 获取路径下文件列表* param bucketName 存储桶* param prefix 文件名称* param recursive 是否递归查找false模拟文件夹结构查找* return 二进制流*/public static IterableResultItem listObjects(String bucketName, String prefix,boolean recursive) {return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());}/*** 使用MultipartFile进行文件上传* param bucketName 存储桶* param file 文件名* param objectName 对象名* param contentType 类型* return* throws Exception*/public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,String objectName, String contentType) throws Exception {InputStream inputStream file.getInputStream();return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).contentType(contentType).stream(inputStream, inputStream.available(), -1).build());}/*** 上传本地文件* param bucketName 存储桶* param objectName 对象名称* param fileName 本地文件路径*/public static String uploadFile(String bucketName, String objectName,String fileName, boolean needUrl) throws Exception {minioClient.uploadObject(UploadObjectArgs.builder().bucket(bucketName).object(objectName).filename(fileName).build());if (needUrl) {String imageUrl fileHost / bucketName / objectName;return imageUrl;}return ;}/*** 通过流上传文件** param bucketName 存储桶* param objectName 文件对象* param inputStream 文件流*/public static ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());}public static String uploadFile(String bucketName, String objectName, InputStream inputStream, boolean needUrl) throws Exception {minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());if (needUrl) {String imageUrl fileHost / bucketName / objectName;return imageUrl;}return ;}/*** 创建文件夹或目录* param bucketName 存储桶* param objectName 目录路径*/public static ObjectWriteResponse createDir(String bucketName, String objectName) throws Exception {return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(new ByteArrayInputStream(new byte[]{}), 0, -1).build());}/*** 获取文件信息, 如果抛出异常则说明文件不存在** param bucketName 存储桶* param objectName 文件名称*/public static String getFileStatusInfo(String bucketName, String objectName) throws Exception {return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()).toString();}/*** 拷贝文件** param bucketName 存储桶* param objectName 文件名* param srcBucketName 目标存储桶* param srcObjectName 目标文件名*/public static ObjectWriteResponse copyFile(String bucketName, String objectName,String srcBucketName, String srcObjectName) throws Exception {return minioClient.copyObject(CopyObjectArgs.builder().source(CopySource.builder().bucket(bucketName).object(objectName).build()).bucket(srcBucketName).object(srcObjectName).build());}/*** 删除文件* param bucketName 存储桶* param objectName 文件名称*/public static void removeFile(String bucketName, String objectName) throws Exception {minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** 批量删除文件* param bucketName 存储桶* param keys 需要删除的文件列表* return*/public static void removeFiles(String bucketName, ListString keys) {ListDeleteObject objects new LinkedList();keys.forEach(s - {objects.add(new DeleteObject(s));try {removeFile(bucketName, s);} catch (Exception e) {log.error(批量删除失败error:{},e);}});}/*** 获取文件外链* param bucketName 存储桶* param objectName 文件名* param expires 过期时间 7 秒 外链有效时间单位秒* return url* throws Exception*/public static String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {GetPresignedObjectUrlArgs args GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();return minioClient.getPresignedObjectUrl(args);}/*** 获得文件外链* param bucketName* param objectName* return url* throws Exception*/public static String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {GetPresignedObjectUrlArgs args GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).method(Method.GET).build();return minioClient.getPresignedObjectUrl(args);}/*** 将URLDecoder编码转成UTF8* param str* return* throws UnsupportedEncodingException*/public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {String url str.replaceAll(%(?![0-9a-fA-F]{2}), %25);return URLDecoder.decode(url, UTF-8);}/****************************** Operate Files End ******************************/}5.开发测试
我刚好在做练手项目这里写个上传头像的接口。
import com.pitayafruits.base.BaseInfoProperties;
import com.pitayafruits.config.MinIOConfig;
import com.pitayafruits.grace.result.GraceJSONResult;
import com.pitayafruits.grace.result.ResponseStatusEnum;
import com.pitayafruits.utils.MinIOUtils;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;RestController
RequestMapping(file)
public class FileController extends BaseInfoProperties {Resourceprivate MinIOConfig minIOConfig;PostMapping(uploadFace)public GraceJSONResult upload(RequestParam MultipartFile file,String userId) throws Exception {if (StringUtils.isBlank(userId)) {return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);}String filename file.getOriginalFilename();if (StringUtils.isBlank(filename)) {return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);}filename face / userId / filename;MinIOUtils.uploadFile(minIOConfig.getBucketName(), filename, file.getInputStream());String faceUrl minIOConfig.getFileHost() / minIOConfig.getBucketName() / filename;return GraceJSONResult.ok(faceUrl);}}可以看到通过工具类只需要一行代码就可以实现上传文件我们只需要在调用的时候做好文件的业务隔离即可。完成了接口的开发这里我来通过Apifox调用测试一下。 通过浏览器访问返回的图片链接会自动下载我们再登录控制台看对应的桶下的这个路径也可以看到这个文件。
小结
我们在集成第三方服务时应遵循一个核心原则将API操作封装成通用工具类。这不仅让MinIO的集成更加优雅也让代码具备更好的复用性和可维护性。这种思维方式同样适用于其他第三方服务的对接。