本节主要是讲解的是分布式文件存储,主要介绍了阿里云OSS云存储和Minio文件存储,本章重点主要是掌握怎么在SpringBoot项目里面接入文件存储。
记录、交流、实践,让每一份付出皆可看见,让你我共同前行😁
1.分布式文件存储高性能高可用讲解
1.1 核心知识介绍
- 数据存储背景:数据量持续攀升,存储单位从 KB、MB、GB、TB、PB 到 ZB 级别,涵盖图片、文档、素材、静态页面、音视频、安装包等各类文件。
- 业务应用内存储问题:传统 javaweb 项目文件量增长后,会占用大量内存、磁盘和带宽,无法满足海量请求,存在开发易但扩容难的问题。
- 分布式文件系统(Distributed File System):
-
- 定义:文件系统管理的物理存储资源通过计算机网络与节点相连,或由不同逻辑磁盘分区组合形成层次化文件系统。
- 特点:自研的分布式文件系统扩容容易,但开发难度大。
1.2 如何保证分布式存储的高性能与高可用?
- 常见思路:采用副本备份、双活、多活等架构,通过复制协议同步数据到多个存储节点,确保数据一致性,故障时自动切换服务。
- 性能与高可用的矛盾(基于 CAP 定理):
-
- 异步复制:先写一份数据到某机器并立即返回,再异步备份。性能好,但存在容错风险(如未同步时节点宕机导致数据丢失)。
- 同步多写:同时写多个副本,全部成功后返回。保证数据一致性,但性能受最慢机器影响,性能下降。
- 选择依据:
-
- 若要求高性能,可接受偶尔文件丢失或访问出错,选异步复制。
- 若要求高可用,需保证数据一致性,选同步多写,牺牲部分性能。
- 类似案例:RocketMQ 消息高可用采用同步双写、异步刷盘策略,即同时写到两个节点内存后返回,再异步持久化到磁盘。
1.3 分布式文件存储业界常见解决方案介绍
解决方案 |
特点 |
MinIO |
Apache License v2.0 下的对象存储服务器,学习、安装运维简单,支持主流语言客户端整合,可与容器化技术结合,社区活跃但不够成熟,参考资料少 |
FastDFS |
开源轻量级分布式文件系统,客户端少(主要 C 和 java),在互联网创业公司应用较多,无官方文档,社区不活跃,架构和部署复杂,问题定位难 |
云厂商(阿里云 OSS、七牛云、腾讯云、亚马逊云等) |
优点:开发简单,功能强大,易维护(支持不同网络下图片质量、水印、加密、扩容、加速等);缺点:收费,个性化处理和未来转移复杂(部分厂商提供一键迁移) |
CDN(Akamai) |
在 CDN 领域表现突出 |
2.Minio
官方网站: MinIO | 企业级高性能对象存储 - MinIO 对象存储
2.1 环境安装
docker run -d -p 9111:9111 -p 9112:9112 --name guslegend_minio \
-e "MINIO_ROOT_USER=XXX" \
-e "MINIO_ROOT_PASSWORD=XXX" \
-v /dev-ops/minio/data:/data \
-v /dev-ops/minio/config:/root/.minio \
minio/minio:RELEASE.2025-04-22T22-12-26Z server /data --console-address ":9112" --address ":9111"
步骤
- 访问控制台
- 创建 bucket
- 上传文件
- 预览
总结
MinIO 操作流畅,支持单机和集群部署。对于不能或不使用云厂商存储服务的场景,可自建 MinIO 对象存储集群。
2.2 项目配置
在父文件夹的pom文件中添加maven依赖,并且在用户服务里面也添加。
<!-- Minio各个项目单独加依赖,根据需要进行添加-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>
微服务配置minio
minio:
endpoint: http://localhost:9111
accesskey:
secretKey:
bucketname:
创建MinioConfig配置类
@Data
@ConfigurationProperties(prefix = "minio")
@Component
public class MinioConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketname;
}
2.3 编码实战
service层开发
@Override
public String uploadFileByMinio(MultipartFile file) {
try {
MinioClient minioClient = MinioClient.builder()
.endpoint(minioConfig.getEndpoint())
.credentials(minioConfig.getAccessKeyId(), minioConfig.getAccessKeySecret())
.build();
//判断桶是否存在
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioConfig.getBucketname()).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioConfig.getBucketname()).build());
}else {
log.info("{}桶,存在",minioConfig.getBucketname());
}
//设置存储对象的名称
String folder= String.format("%s",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));
String fileName = CommonUtil.generateUUID();
String newFileName = folder+fileName+file.getOriginalFilename();
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(minioConfig.getBucketname())
.stream(file.getInputStream(), file.getSize(),-1)
.object(newFileName)
.build();
minioClient.putObject(putObjectArgs);
String imgUrl =minioConfig.getEndpoint()+"/"+minioConfig.getBucketname()+"."+newFileName;
log.info("文件上传地址为:{}",imgUrl);
return imgUrl;
} catch (Exception e) {
log.error("文件上传失败:{}",e);
}
return null;
}
controller层开发
@PostMapping("upload_by_minio")
public JsonData uploadHearImgByMinio(MultipartFile file){
String result = fileService.uploadFileByMinio(file);
return result!=null?JsonData.buildSuccess(result):JsonData.buildResult(BizCodeEnum.FILE_UPLOAD_USER_IMG_FAIL);
}
3.阿里云OSS
3.1 项目配置
在父文件夹的pom文件中添加maven依赖,并且在用户服务里面也添加。
<!-- OSS各个项目单独加依赖,根据需要进行添加-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
在用户服务配置配置文件
aliyun:
oss:
endpoint: XXX
access-key-id: XXX
access-key-secret: XXX
bucketname: XXX
创建OSSConfig配置类
@ConfigurationProperties(prefix = "aliyun.oss")
@Configuration
@Data
public class OSSConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
3.2 编码实战
UUID随机生成工具类开发
/**
* UUID
* @return
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replace("-", "");
}
service层编写
@Slf4j
@Service
public class FileServiceImpl implements FileService {
@Autowired
private OSSConfig ossConfig;
@Override
public String uploadFile(MultipartFile file) {
String originalFileName = file.getOriginalFilename();
//相关配置
String endpoint = ossConfig.getEndpoint();
String accessKeyId = ossConfig.getAccessKeyId();
String accessKeySecret = ossConfig.getAccessKeySecret();
String bucketName = ossConfig.getBucketName();
//创建OSS对象
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String folder = localDateTime.format(formatter);
String fileName = CommonUtil.generateUUID();
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
//在OSS创建shop-user文件夹
String newFileName = "shop-user/"+folder+"/"+fileName+"."+extension;
try {
PutObjectResult result = ossClient.putObject(bucketName, newFileName, file.getInputStream());
//访问路径
if (null!=result){
String imgUrl = "https://"+bucketName+"."+endpoint+"/"+newFileName;
return imgUrl;
}
} catch (Exception e) {
log.error("头像上传失败",e);
}finally {
//关闭OSS对象
ossClient.shutdown();
}
return null;
}
}
controller层编写
@RestController
@RequestMapping("/api/user/v1")
public class UserController {
@Autowired
private FileService fileService;
/**
* 上传用户头像
* @param file
* @return
*/
@PostMapping("upload")
public JsonData uploadHearImg(MultipartFile file){
String result = fileService.uploadFile(file);
return result!=null?JsonData.buildSuccess(result):JsonData.buildResult(BizCodeEnum.FILE_UPLOAD_USER_IMG_FAIL);
}
}