文章目录
一.前言
对象存储是一种数据存储架构
,设计用于管理和处理大量非结构化数据。与传统的文件存储和块存储不同,对象存储通过将数据分解为离散的、独立的单元或“对象”来存储每个对象包含数据本身、相关的元数据和一个唯一的标识符。
以下是对象存储、服务器磁盘和分布式文件系统的对比表格:
特性 | 对象存储 | 服务器磁盘 | 分布式文件系统 |
---|---|---|---|
存储方式 | 以对象为基本单位存储数据,每个对象包含数据、元数据和唯一标识符 | 数据直接存储在服务器的本地磁盘上 | 数据分布在多个服务器节点上,通过网络进行数据访问 |
优点 | 高可扩展性: 能够轻松扩展至数十乃至数百EB的容量。 高效性: 扁平化结构,不受复杂目录系统对性能的影响。 安全性高: 通常凭借HTTP调用对象存储本身提供的认证密钥来提供数据访问。 访问方便: 支持HTTP(S)协议,采用REST的API方式调用和检索数据。 成本相对低: 与块存储方式相比,对象存储是最具成本效益的数据存储类型。 | 开发便捷: 直接使用服务器磁盘,无需复杂的配置和开发工作。 成本低: 如果已有服务器和磁盘,不需要额外支付存储服务费用。 | 容易实现扩容: 可以通过增加更多的服务器节点来扩展存储容量,实现水平扩展。 |
缺点 | 最终一致性 :由于不同节点的位置不同,数据同步时可能会有一定时间的延迟或者错误。 不适合频繁变动的数据 :对象存储比较适合存储那些变动不大甚至不变的文件。 | 扩展困难: 服务器磁盘的容量有限,当存储空间不足时,扩展磁盘容量可能需要更换更大容量的硬盘或者增加额外的磁盘阵列,成本较高且操作复杂。 | 复杂度高: 分布式文件系统的部署和维护相对复杂,需要专业的运维团队来管理。 |
分布式文件系统
分布式文件系统(Distributed File System, DFS)是一种文件系统,它使文件可以跨越多个服务器或存储设备存储和访问。DFS 通过网络将多个存储资源组合成一个统一的文件系统,使用户和应用程序可以像访问本地文件一样透明地访问远程文件。
分布式文件系统的关键特性:
- 透明性:用户和应用程序可以像访问本地文件一样访问远程文件,感受不到底层的复杂性。
- 高可用性:通过复制和冗余机制,确保即使某些节点或硬件发生故障,数据仍然可用。
- 可扩展性:能够处理随着数据量和用户数增加而增长的需求。
- 容错性:通过数据冗余和错误检测机制,保证系统能继续运行,即使发生部分硬件或网络故障。
- 性能:通过分布式架构,能够有效地处理大量并发访问请求。
MINIO
MinIO 是一个高性能、轻量级
的对象存储服务器
,专为大规模数据存储和分析而设计。它兼容 Amazon S3 API
,可以无缝替代 Amazon S3 作为存储后端,并且支持在各种环境中部署,包括物理服务器、虚拟机、容器等。
FastDFS和MINIO的区别
以下是MinIO和FastDFS的对比表格:
特性 | MinIO | FastDFS |
---|---|---|
存储类型 | 对象存储 | 文件存储 |
架构 | 单一守护进程,支持分布式 | Tracker-Storage 分离 |
协议支持 | S3 兼容,支持 HTTP/REST API | 专有协议 |
数据冗余 | 纠删码,多节点和磁盘故障容错 | 主从复制,多副本 |
性能 | 高性能,适合大文件和海量数据存储 | 适合小文件存储,上传/下载性能较高 |
扩展性 | 高度可扩展,支持水平扩展 | 可扩展但管理复杂 |
管理工具 | Web 界面,支持 Prometheus 监控 | 命令行工具 |
生态系统 | 广泛,集成度高 | 生态相对较小,集成度低 |
安装部署 | 简单,开箱即用 | 复杂,需要专业知识 |
社区与支持 | 活跃,有官方文档和社区支持 | 缺乏官方文档和持续更新 |
MinIO和FastDFS各有优势,MinIO在兼容性、扩展性、性能和生态系统方面表现更佳,适合云原生应用、大数据分析等场景;而FastDFS在处理小文件方面性能出色,适合需要海量小文件存储的场景。具体选择可以根据实际需求和使用场景来决定。
二.Windows环境安装Minio
1、下载服务端和客户端安装包文件
下载地址:MinIO | Code and downloads to create high performance object storage
服务端文件:minio.exe 用于接收文件信息
客户端文件:mac.exe 用于上传文件 ,如果用程序代码操作文件存储,只启动服务端就ok
2、启动minio服务器
特别提示:在windows 安装软件我们都习惯双击.exe 文件启动。minio可不行奥,可不行,可不行。千万不能去双击运行,这样可能会导致最终启动失败;无论是windows还是linux都建议通过命令启动的。
以管理员权限打开cmd窗口,进入到minio.exe所在bin目录
2.2、设置用户名
用于登录minio客户端
setx MINIO_ROOT_USER name
设置登录密码
setx MINIO_ROOT_PASSWORD password
2.3、启动Minio服务
.\minio.exe server D:\develpo\minio\data --console-address "127.0.0.1:9000" --address "127.0.0.1:9005"
D:\develpo\minio\data 指定数据存放路径
9005是控制台端口,9000是服务的端口。
4.5、访问minio服务器
访问客户端地址 http://127.0.0.1:9000/ 输入用户密码
使用 Docker 安装
docker run -p 9000:9000 minio/minio server /data
三. 基本概念
基本概念
概念 | 定义 | 特点 |
---|---|---|
Object | 存储到MinIO的基本对象,如文件、字节流等 | 具有唯一标识,可设置元数据 |
Bucket | 用来存储Object的逻辑空间,相当于顶层文件夹 | 数据隔离,命名唯一,可设置权限 |
Drive | 存储数据的磁盘,启动时以参数传入 | 是数据存储载体,容量有限 |
Set | 一组Drive的集合 | 分布式部署自动划分,对象存储于其上,Drive数量固定,尽可能分布在不同节点 |
MinIO 纠删码 EC (Erasure Code)
纠删码(Erasure Code, EC) 是一种数据保护方法,它将数据分割成片段,生成冗余数据块,并将这些数据块存储在不同的位置,如磁盘、存储节点或其他地理位置。MinIO 采用 Reed-Solomon 纠删码实现,将对象拆分成数据块和奇偶校验块,以提高数据的冗余性和可用性。
简单来说就是可以通过数学计算,把丢失的数据进行还原,它可以将n份原始数据,增加m份数据,并能通过n+m份中的任意n份数据,还原为原始数据。
即如果有任意小于等于m份的数据失效,仍然能通过剩下的数据还原出来。
举个最简单例子就是有两个数据(d1, d2),用一个校验和y(d1 + d2 = y)即可保证即使丢失其中一个,依然可以还原数据。如丢失 d1 ,则使用 y - d2 = d1 还原,同理,d2 丢失或者y丢失,均可通过计算得出。
存储形式
当文件对象上传到MinIO时,数据会以特定的形式存储在对应的数据存储磁盘中。具体存储形式如下:
-
目录结构:
- 以Bucket名称为目录。
- 文件名称为下一级目录。
- 文件名下包含编码数据块及检验块(EC码)和元数据文件(xl.meta)。
-
数据块和元数据文件:
- 编码数据块及检验块(EC码):这些块存储在奇数编号的磁盘上,例如data01和data03。
- 元数据文件(xl.meta):这些文件存储在偶数编号的磁盘上,例如data02和data04。
例如,假设我们有四块磁盘:data01、data02、data03、data04。存储形式如下:
-
data01:
/bucket_name/file_name/part.1
(编码数据块)/bucket_name/file_name/part.2
(编码数据块)- …
-
data02:
/bucket_name/file_name/xl.meta
(元数据文件)
-
data03:
/bucket_name/file_name/part.3
(编码数据块)/bucket_name/file_name/part.4
(编码数据块)- …
-
data04:
/bucket_name/file_name/xl.meta
(元数据文件)
这种存储形式确保了数据的冗余和高可用性,通过纠删码技术,即使部分磁盘损坏,数据仍然可以被恢复。
Minio中的存储级别
Minio当前支持两种存储级别:Reduced Redundancy
和Standard
,通过对两种级别的设置来修改对象的Parity Drives
( P)(奇偶校验块)和Data Drives
(D)(数据块)的比例,让用户能够更好的控制磁盘使用率和容错性。
STANDARD
STANDARD
存储级别包含比REDUCED_REDUNDANCY
存储级别更多的奇偶校验块,因此STANDARD
存储级别的奇偶校验块需要满足如下条件:
- 在未设置
REDUCED_REDUNDANCY
存储级别的情况下,STANDARD
存储级别的奇偶校验块需要大于等于2; - 在设置了
REDUCED_REDUNDANCY
存储级别的情况下,STANDARD
存储级别的奇偶校验块需要大于REDUCED_REDUNDANCY
存储级别的奇偶校验块数量 - 奇偶校验块的数量必须小于数据块,所以
STANDARD
存储级别的奇偶校验块不能大于N/2(N为Erasure Set中的磁盘数量)
STANDARD
存储级别的奇偶校验块的默认值取决于Erasure Set
中的磁盘数量:
Erasure Set Size | Default Parity (EC:N) |
---|---|
5 or fewer | EC:2 |
6-7 | EC:3 |
8 or more | EC:4 |
REDUCED_REDUNDANCY
REDUCED_REDUNDANCY
存储级别包含比STANDARD
存储级别更少的奇偶校验块,因此REDUCED_REDUNDANCY存储级别的奇偶校验块需要满足如下条件:
- 在未设置
STANDARD
存储级别的情况下,REDUCED_REDUNDANCY
存储级别的奇偶校验块需要小于N/2 - 在设置了
STANDARD
存储级别的情况下,REDUCED_REDUNDANCY
存储级别的奇偶校验块需要小于STANDARD存储级别的奇偶校验块数量 - 奇偶校验块的数量必须小于数据块,且
REDUCED_REDUNDANCY
存储级别的奇偶校验块需要小于STANDARD
存储级别的奇偶校验块数量,那么REDUCED_REDUNDANCY
存储级别的奇偶校验块需要大于等于2,所以Erasure Set
中的磁盘数量大于4的时候才支持REDUCED_REDUNDANCY
存储级别。
REDUCED_REDUNDANCY
存储级别的奇偶校验块默认值为:EC:2
使用
配置存储级别
有以下两种配置方式:
- 环境变量
export MINIO_STORAGE_CLASS_STANDARD=EC:4
export MINIO_STORAGE_CLASS_RRS=EC:2
设置以上环境变量并重启服务
- mc admin
mc admin config set myminio/ storage_class standard="EC:4"
mc admin config set myminio/ storage_class rrs="EC:2"
# 重启minio服务
mc admin service restart myminio/
存储方案
四. 命令操作
列出存储桶
mc ls <alias>
这会列出指定 MinIO 服务器上的所有存储桶。
创建存储桶
mc mb <alias>/<bucket_name>
这会在指定 MinIO 服务器上创建一个新的存储桶。
上传文件
mc cp <file_path> <alias>/<bucket_name>
这会将本地文件上传到指定的 MinIO 存储桶中。
下载文件
mc cp <alias>/<bucket_name>/<file_name> <local_file_path>
这会将 MinIO 存储桶中的文件下载到本地。
复制对象
mc cp <source> <target>
这会复制对象从一个位置到另一个位置,可以是存储桶内的对象或不同存储桶间的对象。
移动对象
mc mv <source> <target>
这会移动对象从一个位置到另一个位置,与复制不同的是,移动后源位置的对象将被删除。
删除对象
mc rm <alias>/<bucket_name>/<object_name>
这会删除指定的对象。
删除存储桶
mc rb <alias>/<bucket_name>
这会删除指定的存储桶及其中的所有对象。
五 . MinIO 整合 SpringBoot
1、导入依赖:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
2、在SpringBoot的配置文件中编写 MinIO 的配置:
minio:
config:
url: http://127.0.0.1:9005 #ip地址
accessKey: admin # 账号
secretKey: admin962464 # 密码
secure: false #如果是true,则用的是https而不是http,默认值是true
bucketName: "test" # 桶的名字 相当于文件夹
3、编写 MinIO 配置类:
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio.config")
public class MinioConfig {
/**
* 服务地址
*/
private String url;
/**
* 用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
@Bean
public MinioClient getMinioClient() {
return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
}
}
4、编写 MinIO 的工具类:
import com.jjy.shopping_file_service.config.MinioConfig;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Slf4j
@Component
public class MinIOUtil {
@Resource
private MinioConfig minioConfig;
@Resource
private MinioClient minioClient;
/**
* 查看存储bucket是否存在
*
* @param bucketName 存储桶名称
* @return boolean
*/
public Boolean bucketExists(String bucketName) {
Boolean found;
try {
found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return found;
}
/**
* 创建存储bucket
*
* @param bucketName 存储桶名称
* @return Boolean
*/
public Boolean makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 删除存储bucket
*
* @param bucketName 存储桶名称
* @return Boolean
*/
public Boolean removeBucket(String bucketName) {
try {
minioClient.removeBucket(RemoveBucketArgs.builder()
.bucket(bucketName)
.build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 获取全部bucket
*
* @return 存储桶列表
*/
public List<Bucket> getAllBuckets() {
try {
return minioClient.listBuckets();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 文件上传
*
* @param file 文件
* @return 文件对象名称
*/
public String upload(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
System.out.println(originalFilename);
if (!StringUtils.hasText(originalFilename)) {
throw new RuntimeException();
}
String fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String prefix = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
String objectName = prefix + "/" + fileName;
try {
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName)
.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
// 文件名称相同会覆盖
minioClient.putObject(objectArgs);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return objectName;
}
/**
* 预览图片
*
* @param fileName 文件名称
* @return 文件预览链接
*/
public String preview(String fileName) {
// 查看文件地址
GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs
.builder()
.bucket(minioConfig.getBucketName())
.object(fileName).method(Method.GET).build();
try {
String url = minioClient.getPresignedObjectUrl(build);
return url;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 文件下载
*
* @param fileName 文件名称
* @param res response
*/
public void download(String fileName, HttpServletResponse res) {
GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(minioConfig.getBucketName())
.object(fileName).build();
try (GetObjectResponse response = minioClient.getObject(objectArgs)) {
byte[] buf = new byte[1024];
int len;
try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
while ((len = response.read(buf)) != -1) {
os.write(buf, 0, len);
}
os.flush();
byte[] bytes = os.toByteArray();
res.setCharacterEncoding("utf-8");
res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
try (ServletOutputStream stream = res.getOutputStream()) {
stream.write(bytes);
stream.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查看文件对象
*
* @return 存储bucket内文件对象信息
*/
public List<Item> listObjects() {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(minioConfig.getBucketName()).build());
List<Item> items = new ArrayList<>();
try {
for (Result<Item> result : results) {
items.add(result.get());
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return items;
}
/**
* 删除
*
* @param fileName 文件名称
* @return 是否删除成功
*/
public boolean remove(String fileName) {
try {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.build());
} catch (Exception e) {
return false;
}
return true;
}
}
5、Controller:
import com.cyw.miniodemo.config.MinioConfig;
import com.cyw.miniodemo.pojo.Rst;
import com.cyw.miniodemo.service.FileUploadService;
import com.cyw.miniodemo.utils.MinIOUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/file")
@AllArgsConstructor
public class FileUploadController {
private MinioConfig minioConfig;
private MinIOUtil minIOUtil;
private FileUploadService fileUploadService;
@GetMapping("/bucketExists")
public Rst bucketExists(@RequestParam("bucketName") String bucketName) {
Map<String, Object> map = new HashMap<>();
map.put("bucketExists", minIOUtil.bucketExists(bucketName));
return Rst.ok("查询成功", map);
}
@GetMapping("/makeBucket")
public Rst makeBucket(@RequestParam("bucketName") String bucketName) {
Map<String, Object> map = new HashMap<>();
map.put("makeBucketSuccess", minIOUtil.makeBucket(bucketName));
return Rst.ok("创建成功", map);
}
@GetMapping("/removeBucket")
public Rst removeBucket(@RequestParam("bucketName") String bucketName) {
Map<String, Object> map = new HashMap<>();
map.put("deleteBucketSuccess", minIOUtil.removeBucket(bucketName));
return Rst.ok("删除成功", map);
}
@GetMapping("/getAllBuckets")
public Rst getAllBuckets() {
Map<String, Object> map = new HashMap<>();
map.put("buckets", minIOUtil.getAllBuckets());
return Rst.ok("查询成功", map);
}
@PostMapping("/upload")
public Rst upload(@RequestParam("file") MultipartFile file) {
String objectName = minIOUtil.upload(file);
if (objectName != null) {
Map<String, Object> map = new HashMap<>();
map.put("url", (minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + "/" + objectName));
return Rst.ok("上传成功", map);
}
return Rst.fail("上传失败");
}
@GetMapping("/preview")
public Rst preview(@RequestParam("fileName") String fileName) {
Map<String, Object> map = new HashMap<>();
map.put("url", minIOUtil.preview(fileName));
return Rst.ok("预览成功", map);
}
@GetMapping("/download")
public Rst download(@RequestParam("fileName") String fileName, HttpServletResponse resp) {
minIOUtil.download(fileName, resp);
return Rst.ok();
}
@PostMapping("/delete")
public Rst remove(@RequestBody Map<String, String> params) {
String url = params.get("url");
String objName = url.substring(url.lastIndexOf(minioConfig.getBucketName() + "/") + minioConfig.getBucketName().length() + 1);
log.info("删除对象: {}", objName);
minIOUtil.remove(objName);
return Rst.ok("删除成功");
}
}
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力