重学SpringBoot系列之整合分布式文件系统

x33g5p2x  于2021-12-05 转载在 Spring  
字(25.6k)|赞(0)|评价(0)|浏览(405)

文件本地上传与提供访问服务

本章的核心内容是为大家介绍分布式文件系统,用于存储应用的图片、word、excel、pdf等文件。在开始介绍分布式文件系统之前,为大家介绍一下使用本机存储来存放文件资源。

二者的核心实现过程是一样的:

上传文件,保存文件(本节是本地磁盘)

返回文件HTTP访问服务路径给前端,进行上传之后的效果展示

复习

服务端接收上传的目的是提供文件的访问服务,那么对于SpringBoot而言,有哪些可以提供文件访问的静态资源目录呢?

  • classpath:/META-INF/resources/ ,
  • classpath:/static/ ,
  • classpath:/public/ ,
  • classpath:/resources/

这是之前的章节,我们为大家介绍的内容,从这里看出这里的静态资源都在classpath下。那么就出现问题:

  • 应用的文件资源不能和项目代码分开存储(你见过往github上传代码,还附带项目文件数据的么?)
  • 项目打包困难,当上传的文件越来越多,项目的打包jar越来越大。
  • 代码与文件数据不能分开存储,就意味着文件数据的备份将变得复杂

文件上传目录自定义配置

怎么解决上述问题?别忘记了spring boot 为我们提供了使用spring.resources.static-locations配置自定义静态文件的位置。

web:
  upload-path: D:/data/

spring:
  resources:
    static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${web.upload-path}
  • 配置web.upload-path为与项目代码分离的静态资源路径,即:文件上传保存根路径
  • 配置spring.resources.static-locations,除了带上Spring Boot默认的静态资源路径之外,加上file:${web.upload-path}指向外部的文件资源上传路径。该路径下的静态资源可以直接对外提供HTTP访问服务。

spring.resources.static-locations的配置会覆盖springboot默认的四个静态资源配置

文件上传的Controller实现

详情看代码注释

@RestController
public class FileUploadController {

  //绑定文件上传路径到uploadPath
  @Value("${web.upload-path}")
  private String uploadPath;

  SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");

  @PostMapping("/upload")
  public String upload(MultipartFile uploadFile,
                       HttpServletRequest request) throws IOException {

    // 在 uploadPath 文件夹中通过日期对上传的文件归类保存
    // 比如:/2019/06/06/cf13891e-4b95-4000-81eb-b6d70ae44930.png
    String format = sdf.format(new Date());
    File folder = new File(uploadPath + format);
    if (!folder.isDirectory()) {
      folder.mkdirs();
    }

    // 对上传的文件重命名,避免文件重名
    String oldName = uploadFile.getOriginalFilename();
    String newName = UUID.randomUUID().toString()
            + oldName.substring(oldName.lastIndexOf("."), oldName.length());

    // 文件保存
    uploadFile.transferTo(new File(folder, newName));

    // 返回上传文件的访问路径
    //https://localhost:8888/2020/10/18/a9a05df4-6615-4bb5-b859-a3f9bf4bfae0.jpg
    //request.getScheme() 返回当前链接使用的协议;比如,一般应用返回http;SSL返回https;
    //request.getServerName()可以返回当前页面所在的服务器的名字,就是上面例子中的“localhost"
    String filePath = request.getScheme() + "://" + request.getServerName()
            + ":" + request.getServerPort() + "/"   + format + newName;
    return filePath;
  }

}

写一个模拟的文件上传页面,进行测试

把该upload.html文件放到classpath:public目录下,对外提供访问。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="uploadFile" value="请选择上传文件">
    <input type="submit" value="保存">
</form>
</body>
</html>

访问测试、点击“选择文件”,之后保存

文件被保存到服务端的web.upload-path指定的资源目录下

浏览器端响应结果如下,返回一个文件HTTP访问路径:

使用该HTTP访问路径,在浏览器端访问效果如下。证明我们的文件已经成功上传到服务端,以后需要访问该图片就通过这个HTTP URL就可以了。

MinIO简介与选型介绍

中文文档

目前可用于文件存储的网络服务选择有很多,比如阿里云OSS、七牛云、腾讯云等等,但是收费都有点小贵。

为什么使用MInIO替换了FastDFS

MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

理由一:安装部署(运维)复杂度

如果将一个fastDFS分布式服务部署完成,需要具备以下的知识

  • linux基础的目录操作
  • 常用的分布式主从原理
  • C语言代码的编译
  • nginx安装部署
  • nginx插件的使用(防盗链)

如果仅仅是上面的这些基础知识,安排几个程序员学一学还好说。主要是fastdfs的部署结构之复杂,如果我长时间不回顾,自己都会忘了这复杂的架构是怎么回事。

当我看到MinIO的安装过程之后,以及分布式的部署命令之后(分布式MinIO快速入门),放弃fastDFS的决心就已经做出了一大半。

说白了:FastDFS的部署不过是零件的组装过程,需要你去理解fastDFS的架构设计,才能够正确的安装部署。MinIO在安装的过程是黑盒的,你不用去深入关注它的架构,也不需要你进行零件组装,基本上可以做到开箱即用。普通的技术人员就能够参与后期运维。

理由二:文档

我觉得从我知道fastDFS开始,也有十年了。竟然没有官方文档,所有的文档全是某某公司的自己总结的文档,或者是某某网友自己总结的文档。

从这点上看fastDFS真的是一败涂地,当然阿里余庆大神在做这个项目的时候可能也没有考虑到后来会有这么多人用。即使用的人多了,在余庆大神眼里可能觉得只是自己开发的一个小玩具,没有继续深入运营的必要。

理由三:开源项目运营组织

fastdfs是阿里余庆做的一个个人项目,在一些互联网创业公司中有应用,没有官网,不活跃,6个contributors。目前已经很少做更新。

MinIO目前是由2014年在硅谷创立的公司MinIO.Inc运营的开源项目,社区论坛的活跃度目前也非常的不错。

理由四:UI界面

我们都知道fastDFS默认是不带UI界面的,看看MinIO的界面吧。这个界面不需要你单独的部署,和服务端一并安装。开箱即用,爱了爱了。

理由五:性能

MinIO号称是世界上速度最快的对象存储服务器。在标准硬件上,对象存储的读/写速度最高可以达到183 GB/s和171 GB/s。关于fastDFS我曾经单线程测试写了20万个文件,总共200G,大约用时10个小时。总体上是很难达到MinIO“号称的”以G为单位的每秒读写速度。

理由六:容器化支持

MinIO提供了与k8s、etcd、docker等容器化技术深度集成方案,可以说就是为了云环境而生的。这点是FastDFS不具备的。

理由七:丰富的SDK支持

fastDFS目前提供了 C 和 Java SDK ,以及 PHP 扩展 SDK。下图是MinIO提供的SDK支持,MinIO几乎提供了所有主流开发语言的SDK以及文档。

理由八:AWS S3标准兼容

Amazon的S3 API是对象存储领域的事实标准。MinIO是S3兼容性的事实上的标准,是第一个采用API和第一个添加对S3 Select支持的标准之一。包括微软Azure在内的750多家公司使用MinIO的S3网关,这一数字超过了业内其他公司的总和。

什么意思?就是说你现在为了节约成本使用MinIO,等你的公司壮大了、有钱了。不想自己运维基础设施了,你就可以把对象存储放到云上,只要云厂商支持S3标准(比如阿里云OSS、七牛云等),你的应用程序是不需要重新开发的。

MinIO的安装与基础用法

MInIO在linux服务器上安装

MInIO的安装有很多方法、单实例的、集群分布式的、docker部署的、支持k8s的,我们这里只给大家介绍最简单的一种安装方式:linux单节点安装。

因为我们课程的主要目的不是为大家讲MinIO,我们的课程主要目的是在Spring Boot应用中集成MinIO的API,操作MInIO进行对象存储,也就是下2节的内容。

如果希望对MinIO有深入的掌握,访问MinIO官网:https://min.io

下载及准备工作

下载地址:https://min.io/download#/linux

二进制源码安装:

cd /root/minio  //自定义一个minio软件操作目录
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio

创建minio文件存储目录及日志目录

mkdir -p /usr/local/data/minio;
mkdir -p /usr/local/logs/minio;

docker安装minio

1.从Docker Hub查找镜像 minio镜像

docker search minio

能够查找到镜像 minio/minio 排在第一位

2.minio安装 (使用docker安装)

docker pull minio/minio

创建容器,并启动

docker run --name minio \
-p 9000:9000 \
-p 9090:9090 \
-d --restart=always \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=admin123" \
-v /usr/local/minio/data:/data \
-v /usr/local/minio/config:/root/.minio \
minio/minio server /data \
--console-address '0.0.0.0:9090'

注意,这里要单独设置console的端口,不然会报错,且无法访问

这种安装方式 MinIO 自定义 Access 和 Secret 密钥要覆盖 MinIO 的自动生成的密钥

登录客户端(浏览器):注意—>此处的端口,是你设置的console的端口:9090

此处的用户名密码为启动服务时,设置的用户名密码:admin admin123:

docker安装minio可以参考最新版本的官方文档

启动MinIO

将下面的内容保存为**/root/minio/run.sh**,与minio下载文件放在同一个目录下面。并为其赋予执行权限chmod u+x run.sh

#!/bin/bash
export MINIO_ACCESS_KEY=dhy
export MINIO_SECRET_KEY=123456
# nohup启动服务 指定文件存放路径 /root/data 还有设置日志文件路径 /root/minio/log
nohup ./minio server /root/data/minio > /root/logs/minio/minio.log 2>&1 &

开启防火请端口,对外提供服务

minio默认的服务端口是9000,需要开放防火墙端口。下面的命令是CentOS7的防火墙端口开放命令:

firewall-cmd --zone=public --add-port=9000/tcp --permanent
firewall-cmd --reload
# 查看是否开放
firewall-cmd --query-port=9000/tcp

访问 http://虚拟机ip:9000/ 进行登录。下图是MinIO的登陆界面

登陆的用户名和密码,使用MINIO_ACCESS_KEY和MINIO_SECRET_KEY的配置值。登录成功之后的首页

MinIO系统的基本用法

创建 bucket

登录之后在浏览器上面,点击右下角“红色的加号”创建 bucket 来存储对象。我们要了解什么是bucket,说白了就是上传的对象文件的分类

  • 你可以按这个图片或者其他资源文件属于哪个系统创建一个bucket,比如说我创建一个boot-launch的bucket给boot-launch应用使用
  • 你也可以按照资源的类型去创建bucket,比如:image,video,audio分别放在不同的bucket中存放

上传资源

bucket 创建好之后,我们就可以向这个bucket里面上传资源文件对象了。点击下图中的按钮

比如我上传了一张png的图片,上传后列表内展示效果如下:

在资源后面的四个按钮分别是:资源分享、预览、下载、删除。

资源分享

MinIO 默认的策略是分享地址的有效时间最多是7天。我们点击Copy link可以获得资源的访问链接

永久资源分享

MinIO 默认的策略是分享地址的有效时间最多是7天,要突破这种限制,可以在 bucket 中进行策略设置。

点击对应的 bucket ,edit policy添加策略*.*,Read Only,如下:

如此就放开了访问,并且没有时间限制,同时只需要按http://${MINIO_HOST}:${MINIO_PORT}/${bucketName}/${fileName}的格式可直接访问资源(不需要进行分享操作)。

在 html 文件中引用静态资源

通过上面的设置与运行,MinIO 作为静态资源服务器已经完成,可以写个 html 来引用 MinIO 中的静态资源。如下是测试的 html 里面的图片、视频、音频均使用 MinIO 的资源地址。

格式:

<img src="http://${MINIO_HOST}:${MINIO_PORT}/image/test.jpg" alt="图片">

实例:

<img src="http://192.168.161.3:9000/boot-launch/java9-streamapi.png" alt="图片">

整合MinIO的JavaSDK

整合MinIO

pom.xml引入:

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>7.1.0</version>
</dependency>

application.yml,服务信息要和我们上一节安装的MinIO服务一致,这样我们才能正常连接测试:

# MinIo文件服务器
minio:
  endpoint: http://服务器ip:9000
  accessKey: dhy
  secretKey: 123456

MinIoProperties.java 配置实体,将上文配置文件属性装载到实体配置对象中:

@Data
@ConfigurationProperties(prefix = "minio")
public class MinIOProperties {
    private String endpoint;
    private String accessKey;
    private String secretKey;
}

写一个工具类,这个工具类只为大家演示了基础的API。更多的API请参考官方文档:https://docs.min.io/cn/java-client-api-reference.html

@Component
@Configuration
@EnableConfigurationProperties({MinIOProperties.class})
public class MinIOTemplate {

    private MinIOProperties minIo;

    public MinIOTemplate(MinIOProperties minIo) {
        this.minIo = minIo;
    }

    private MinioClient instance;

    @PostConstruct //minio操作对象实例化
    public void init() {
        instance = MinioClient.builder()
                 .endpoint(minIo.getEndpoint())
                 .credentials(minIo.getAccessKey(), minIo.getSecretKey())
                 .build();
    }

    /** * 判断 bucket是否存在 */
    public boolean bucketExists(String bucketName)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException,
            ServerException, InternalException, XmlParserException,
            InvalidBucketNameException, ErrorResponseException {

            return instance.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /** * 创建 bucket */
    public void makeBucket(String bucketName) throws IOException, InvalidResponseException,
            InvalidKeyException, NoSuchAlgorithmException, ServerException,
            ErrorResponseException, XmlParserException, InvalidBucketNameException,
            InsufficientDataException, InternalException, RegionConflictException {

            boolean isExist = this.bucketExists(bucketName);
            if(!isExist) {
                instance.makeBucket(MakeBucketArgs.builder()
                                 .bucket(bucketName)
                                 .build());
            }
    }

    /** * 文件上传 * @param bucketName bucket名称 * @param objectName 对象名称,文件名称 * @param filepath 文件路径 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, String filepath)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException, ServerException,
            InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {

            return instance.uploadObject(
                         UploadObjectArgs.builder()
                             .bucket(bucketName)
                             .object(objectName)
                             .filename(filepath).build());
    }

    /** * 文件上传 * @param bucketName bucket名称 * @param objectName 对象名称,文件名称 * @param inputStream 文件输入流 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, InputStream inputStream)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException, ServerException,
            InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {

            return instance.putObject(
                         PutObjectArgs.builder()
                                 .bucket(bucketName)
                                 .object(objectName).stream(
                                 inputStream, -1, 10485760)
                                 .build());

    }

    /** * 删除文件 * @param bucketName bucket名称 * @param objectName 对象名称 */
    public void removeObject(String bucketName, String objectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
            NoSuchAlgorithmException, ServerException, InternalException, XmlParserException,
            InvalidBucketNameException, ErrorResponseException {

            instance.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());

    }
}

测试

@SpringBootTest
public class MinIOTest {

  @Resource
  MinIOTemplate minTemplate;

  //测试创建bucket
  @Test
  void testCreateBucket() throws Exception {
    minTemplate.makeBucket("test");
  }

  //测试上传文件对象
  @Test
  void testPutObject() throws Exception {
    ObjectWriteResponse response = minTemplate.putObject("test",
            "base/dhy.png",
            "C:\\Users\\a\\Pictures\\dhy.png");
    System.out.println(response.object());
  }

  //测试删除文件对象
  @Test
  void testDeleteObject() throws Exception {
    minTemplate.removeObject("test",
            "base/dhy.png");
  }
}

第一个测试用例创建一个名称为test的bucket

第二个测试用例上传了一个图片文件对象“base/dhy.png”

第三个测试用例把图片文件对象“base/dhy.png"删除

看上图,需要注意的是当文件对象名称中包含文件夹分隔符“/”的时候,会自动创建文件目录。如上图中的base目录,是上传文件的时候自动创建的。

  • MinIO不提供文件目录的创建API,文件目录随着文件上传动作创建。objectName
    可以是/temp/xxx.jpg,可以认为自动创建了temp目录。
  • MinIO 不提供删除目录的API,把文件目录下的所有文件都删除,就等于文件目录被删除。

自定义一个minio-spring-boot-starter

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.dhy</groupId>
    <artifactId>minio-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

<!-- 引入spring boot父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
    </parent>

    <dependencies>
<!-- 引入Minio的依赖 -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>7.1.0</version>
        </dependency>
        <!-- 引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
<!-- 引入springboot starter依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
<!--绑定配置类和对应的配置文件中的属性值,Idea有提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

</project>

配置类

@Data
@ConfigurationProperties(prefix = "minio")
public class MinioProperties
{
    private String endpoint;//minio服务器地址,不是客户端地址
    private String accessKey;//用户名
    private String secretKey;//密码
}

minio模板类

public class MinIOTemplate {

    private MinioProperties minIo;

    public MinIOTemplate(MinioProperties minIo) {
        this.minIo = minIo;
    }

    private MinioClient instance;

    //Constructor >> @Autowired >> @PostConstruct
    @PostConstruct //minio操作对象实例化
    public void init() {
        instance = MinioClient.builder()
                 .endpoint(minIo.getEndpoint())
                 .credentials(minIo.getAccessKey(), minIo.getSecretKey())
                 .build();
    }

    /** * 判断 bucket是否存在 */
    public boolean bucketExists(String bucketName)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException,
            ServerException, InternalException, XmlParserException,
            InvalidBucketNameException, ErrorResponseException {

            return instance.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /** * 创建 bucket */
    public void makeBucket(String bucketName) throws IOException, InvalidResponseException,
            InvalidKeyException, NoSuchAlgorithmException, ServerException,
            ErrorResponseException, XmlParserException, InvalidBucketNameException,
            InsufficientDataException, InternalException, RegionConflictException {

            boolean isExist = this.bucketExists(bucketName);
            if(!isExist) {
                instance.makeBucket(MakeBucketArgs.builder()
                                 .bucket(bucketName)
                                 .build());
            }
    }

    /** * 文件上传 * @param bucketName bucket名称 * @param objectName 对象名称,文件名称 * @param filepath 文件路径 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, String filepath)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException, ServerException,
            InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {

            return instance.uploadObject(
                         UploadObjectArgs.builder()
                             .bucket(bucketName)
                             .object(objectName)
                             .filename(filepath).build());
    }

    /** * 文件上传 * @param bucketName bucket名称 * @param objectName 对象名称,文件名称 * @param filepath 文件路径 * @param contentType 文件类型--->video/mp4 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, String filepath,String contentType)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException, ServerException,
            InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {

        return instance.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(filepath)
                        .contentType(contentType)
                        .build());
    }

    /** * 文件上传 * @param bucketName bucket名称 * @param objectName 对象名称,文件名称 * @param inputStream 文件输入流 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, InputStream inputStream)
            throws IOException, InvalidKeyException, InvalidResponseException,
            InsufficientDataException, NoSuchAlgorithmException, ServerException,
            InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {

            return instance.putObject(
                         PutObjectArgs.builder()
                                 .bucket(bucketName)
                                 .object(objectName).stream(
                                 inputStream, -1, 10485760)
                                 .build());

    }

    /** * 删除文件 * @param bucketName bucket名称 * @param objectName 对象名称 */
    public void removeObject(String bucketName, String objectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException,
            NoSuchAlgorithmException, ServerException, InternalException, XmlParserException,
            InvalidBucketNameException, ErrorResponseException {

            instance.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());

    }
}

minio自动装配类

//minio自动装配类
@Configuration
//@EnableConfigurationProperties 相当于把使用 @ConfigurationProperties的类注入。
@EnableConfigurationProperties({MinioProperties.class})
//没有MinioAutoConfigure类型的bean时,当前配置类才会生效
@ConditionalOnMissingBean(type ="org.dhy.autoconfigure")
public class MinioAutoConfigure
{
    private final MinioProperties minIo;

    @Autowired
    public MinioAutoConfigure(MinioProperties minIo)
    {
        this.minIo=minIo;
    }

    @Bean
    @ConditionalOnMissingBean
    public MinIOTemplate minIOTemplate()
    {
        return new MinIOTemplate(minIo);
    }

}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dhy.autoconfigure.MinioAutoConfigure

gitee仓库地址

https://gitee.com/DaHuYuXiXi/minio

fastdfs简介及架构说明

简介

  • FastDFS是一个轻量级的开源分布式文件系统。
  • FastDFS主要解决了大容量的文件存储和高并发访问的问题,文件存取时实现了负载均衡。
  • FastDFS实现了软件方式的RAID,可以使用廉价的IDE硬盘进行存储
  • 支持存储服务器在线扩容
  • 支持相同内容的文件只保存一份,节约磁盘空间
  • FastDFS特别适合大中型网站使用,用来存储资源文件(如:图片、文档、音频、视频等等)

架构说明

  • Tracker:管理集群,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。
  • Storage:实际保存文件 Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,

组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

说明 nginx + fileid(文件路径),http访问

好处:

  • 将文件的管理与具体业务应用解耦,可以多个应用共用一套fastDFS集群,分成不同的组
  • 图片访问,只需要将http-url交给浏览器。nginx提供访问服务。
  • 方便统一备份,一组的多个storage就是彼此的备份
  • 可以将图片浏览,文件下载的压力分散给nginx服务。应用自己专心做业务。
  • 缩略图,防盗链等等

使用docker安装fastdfs

安装

拉取镜像

# docker pull delron/fastdfs
Using default tag: latest
latest: Pulling from delron/fastdfs
43db9dbdcb30: Pull complete 
85a9cd1fcca2: Pull complete 
c23af8496102: Pull complete 
e88c36ca55d8: Pull complete 
492fed9ec7f3: Pull complete 
0c1d41dbb2bd: Pull complete 
99b513124929: Pull complete 
bf3f5901a13d: Pull complete 
88bf4f57c2c5: Pull complete 
Digest: sha256:f3fb622783acee7918b53f8a76c655017917631c52780bebb556d29290955b13
Status: Downloaded newer image for delron/fastdfs

创建本机存储目录

rm -fR /home/docker/fastdfs/{tracker,storage} 
mkdir /home/docker/fastdfs/{tracker,storage}  -p

启动tracker

docker run -d \
--network=host \
--name tracker \
-v /home/docker/fastdfs/tracker:/var/fdfs \
delron/fastdfs tracker

启动storage

docker run -d \
--network=host \
--name storage \
-e TRACKER_SERVER=192.168.161.3:22122 \
-v /home/docker/fastdfs/storage:/var/fdfs \
-e GROUP_NAME=group1  \
delron/fastdfs storage

开启宿主机防火墙端口,morunchang/fastdfs镜像在构建的时候为nginx配置的端口为8888

firewall-cmd --zone=public --add-port=22122/tcp --permanent
firewall-cmd --zone=public --add-port=23000/tcp --permanent
firewall-cmd --zone=public --add-port=8888/tcp --permanent
firewall-cmd --reload
# 查看是否开放
firewall-cmd --query-port=22122/tcp
firewall-cmd --query-port=23000/tcp
firewall-cmd --query-port=8888/tcp

测试一下安装结果

FastDFS安装包中,自带了客户端程序,可以使用这个命令行客户端进行文件上传及下载测试。

在宿主机执行命令

3.1上传文件(是容器里面的文件)

# docker exec -i storage /usr/bin/fdfs_upload_file /etc/fdfs/client.conf ./README
group1/M00/00/00/wKgBW10lZHCAC8TaAAAAMT6WPfM3645854

3.2 查看fastdfs文件系统信息

# docker exec -i storage fdfs_file_info /etc/fdfs/client.conf group1/M00/00/00/wKgBW10lZHCAC8TaAAAAMT6WPfM3645854
source storage id: 0
source ip address: 192.168.1.91
file create timestamp: 2019-07-10 04:07:12
file size: 49
file crc32: 1050033651 (0x3E963DF3)

3.3 下载文件,不会下载到宿主机,去容器里面看

# docker exec -i storage fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/wKgBW10lZHCAC8TaAAAAMT6WPfM3645854

3.4 查看集群状态

# docker exec -i storage  fdfs_monitor /etc/fdfs/storage.conf

开发一个自定义fastdfs-starter

主要实现的功能

  • 实现FastDFSClientUtil及properties的自动装配(即:如何开发一个自定义的spring-boot-starter)
  • 加入连接线程池管理(非重点)

实现FastDFSClientUtil及properties的自动装配

实际上就是要自己实现一个starter

1.第一步:maven的依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.dhy.spring</groupId>
  <artifactId>dhy-fastdfs-spring-boot-starter</artifactId>
  <version>1.0.0</version>
  
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
  </parent>

  <dependencies>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>

   <dependency>
      <groupId>net.oschina.zcx7878</groupId>
      <artifactId>fastdfs-client-java</artifactId>
      <version>1.27.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.0</version>
    </dependency>

  </dependencies>
</project>

注意其中 spring-boot-configuration-processor 的作用是编译时生成spring-configuration-metadata.json, 此文件主要给IDE使用,用于提示使用。

如在intellij idea中,当配置此jar相关配置属性在application.yml, 你可以用ctlr+鼠标左键,IDE会跳转到你配置此属性的类中。

fastdfs-client-java和commons-pool2先不用管他们两个是实现fastdfs功能及连接池的,与自动装配无关

2 .第二步:fastdfs属性类

@ConfigurationProperties(prefix = "dhy.fastdfs")
public class FastDFSProperties {

    private Integer connect_timeout = 5;
    private Integer network_timeout = 30;
    private String charset = "UTF-8";
    private List<String> tracker_server = new ArrayList<>();
    private Integer max_total;
    private Boolean http_anti_steal_token = false;
    private String http_secret_key = "";
    private Integer http_tracker_http_port = 8987;
    //下面这个实际上不是fastdfs的属性,为了方便实用自定义属性,表示访问nginx的http地址
    private String httpserver;

    public Integer getHttp_tracker_http_port() {
        return http_tracker_http_port;
    }

    public void setHttp_tracker_http_port(Integer http_tracker_http_port) {
        this.http_tracker_http_port = http_tracker_http_port;
    }

    public Boolean getHttp_anti_steal_token() {
        return http_anti_steal_token;
    }

    public void setHttp_anti_steal_token(Boolean http_anti_steal_token) {
        this.http_anti_steal_token = http_anti_steal_token;
    }

    public String getHttp_secret_key() {
        return http_secret_key;
    }

    public void setHttp_secret_key(String http_secret_key) {
        this.http_secret_key = http_secret_key;
    }

    public Integer getMax_total() {
        return max_total;
    }

    public void setMax_total(Integer max_total) {
        this.max_total = max_total;
    }

    public String getHttpserver() {
        return httpserver;
    }

    public void setHttpserver(String httpserver) {
        this.httpserver = httpserver;
    }

    public List<String> getTracker_server() {
        return tracker_server;
    }

    public void setTracker_server(List<String> tracker_server) {
        this.tracker_server = tracker_server;
    }

    public Integer getConnect_timeout() {
        return connect_timeout;
    }

    public void setConnect_timeout(Integer connect_timeout) {
        this.connect_timeout = connect_timeout;
    }

    public Integer getNetwork_timeout() {
        return network_timeout;
    }

    public void setNetwork_timeout(Integer network_timeout) {
        this.network_timeout = network_timeout;
    }

    public String getCharset() {
        return charset;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }
}

第三步:自动装配配置类

/** * 实现最终目标把FastDFSClientUtil自动注入Spring,提供外部使用 * 使用方式 @Resource、 @Autowired */
@Configuration
//当classpath下面有这三个类才做自动装配
@ConditionalOnClass(value = {FastDFSClientFactory.class,FastDFSClientPool.class,FastDFSClientUtil.class,})
//@EnableConfigurationProperties 相当于把使用 @ConfigurationProperties的类注入。
@EnableConfigurationProperties(FastDFSProperties.class)
public class FastDFSAutoConfigure {

    private final FastDFSProperties properties;

    @Autowired
    public AutoConfigure(FastDFSProperties properties) {
        this.properties = properties;
    }

    @Bean
    @ConditionalOnMissingBean //当没有FastDFSClientUtil,就把FastDFSClientUtil作为Bean注入Spring
    FastDFSClientUtil fastDFSClientUtil (){
        return new FastDFSClientUtil(properties);
    }

}

最后一步,在resources/META-INF/下创建spring.factories文件,内容供参考下面:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.dhy.spring.fastdfs.AutoConfigure

如果有多个自动配置类,用逗号分隔换行即可。

测试自动装配的结果

application.yml

dhy:
fastdfs:
  httpserver: http://192.168.1.91:80/
  connect_timeout: 5
  network_timeout: 30
  charset: UTF-8
  tracker_server:
    - 192.168.1.91:22122
  max_total: 50
  http_anti_steal_token:false
  http_secret_key:

总结下Starter的工作原理:

Spring Boot在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包
根据spring.factories配置加载AutoConfigure类
根据 @Conditional 注解的条件,进行自动配置并将Bean注入Spring Context

整合fastdfs操作文件数据

整合fastdfs

引入maven依赖坐标

git clone 本项目之后,本地install,引入

<dependency>
            <groupId>com.dhy.spring</groupId>
            <artifactId>dhy-fastdfs-spring-boot-starter</artifactId>
            <version>1.0.0</version>
        </dependency>

在application.yml里面加上如下配置:

dhy:
  fastdfs:
    httpserver: http://192.168.161.3:8888/ #这个不是fastdfs属性,但是填上之后,在使用FastDFSClientUtil会得到完整的http文件访问路径
    connect_timeout: 5
    network_timeout: 30
    charset: UTF-8
    tracker_server: # tracker_server 可以配置成数组
      - 192.168.161.3:22122
    max_total: 50
    http_anti_steal_token: false # 如果有防盗链的话,这里true
    http_secret_key: # 有防盗链,这里填secret_key

使用方式

// 使用fastDFSClientUtil提供的方法上传、下载、删除
    @Resource
    FastDFSClientUtil fastDFSClientUtil;

测试一下

@Controller
@RequestMapping("fastdfs")
public class FastdfsController {

    @Resource
    private FastDFSClientUtil fastDFSClientUtil;

    @PostMapping("/upload")
    @ResponseBody
    public AjaxResponse upload(@RequestParam("file") MultipartFile file) {

        String fileId;
        try {
            String originalfileName = file.getOriginalFilename();
            fileId = fastDFSClientUtil.uploadFile(file.getBytes(),originalfileName.substring(originalfileName.lastIndexOf(".")));
            return AjaxResponse.success(fastDFSClientUtil.getSourceUrl(fileId));
        } catch (Exception e) {
            throw new CustomException(CustomExceptionType.SYSTEM_ERROR,"文件上传图片服务器失败");
        }
    }

    @DeleteMapping("/delete")
    @ResponseBody
    public AjaxResponse upload(@RequestParam String fileid) {
        try {
            fastDFSClientUtil.delete(fileid);
        } catch (Exception e) {
            throw new CustomException(CustomExceptionType.SYSTEM_ERROR,"文件删除失败");
        }
       return  AjaxResponse.success();
    }

}

postman文件上传配置:

相关文章

微信公众号

最新文章

更多