一、分布式文件存储 - FastDFS

1.1、FastDFS 简介

1、简介

FastDFS 是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS 很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

2、架构分析

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

image-20210614213652278

1.2、文件上传流程

1、工作流程

多个组集合称为集群,一个组的不同机器称为同步备份。

image-20210614235838198

  • Storage 定时将自己注册到 Tracker 控制注册中心
  • 用户通过文件上传接口将文件上传到服务器中
  • 从 Tracker 中找到可用的 Storage 并返回
  • 根据返回的 Storage 找到可用的 Storage 并实现文件管理

image-20210615000146539

2、组名

文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。

3、虚拟磁盘路径

storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了 store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。

4、数据两级目录

storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。

5、文件名

与文件上传时不同。是由存储服务器根据特定信息生成。

文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

1.3、在虚拟机上搭建 FastDFS

使用 Docker 搭建 FastDFS 开发环境

1、拉取镜像

1
docker pull morunchang/fastdfs

2、运行 tracker

1
docker run --name tracker -d --net=host morunchang/fastdfs sh tracker.sh

3、运行 storager

1
docker run ‐d ‐‐name storage ‐‐net=host ‐e TRACKER_IP=<your tracker server ip>:22122 ‐e GROUP_NAME=<group name> morunchang/fastdfs sh storage.sh
  • 使用的网络模式为 – net = host ,需要替换为你的机器 ip
  • group name ,即为 storage 的组
  • 如果想要增加新的storage服务器,再次运行该命令,注意更换新组名

4、修改 storage 中 nginx 中的配置

  • 进入 storage 的容器内部,修改 nginx.conf
1
docker exec ‐it storage	/bin/bash
  • 修改 nginx 配置文件
1
vi /data/nginx/conf/nginx.conf

添加以下内容

1
2
3
4
5
6
7
8
location /group1/M00 {
proxy_next_upstream http_502 http_504 error timeout invalid_header;
proxy_cache http‐cache;
proxy_cache_valid 200 304 12h;
proxy_cache_key $uri$is_args$args;
proxy_pass http://fdfs_group1;
expires 30d;
}
  • 退出容器
1
exit
  • 重启容器
1
docker restart storage

二、搭建文件管理微服务

2.1、创建文件管理微服务

这个微服务用于进行文件管理

1、引入依赖

1
2
3
4
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
</dependency>

2、在 resources 文件夹下创建 FastDFS 的配置文件 fdfs_client.conf

在这个文件下配置 FastDFS 的连接信息

1
2
3
4
5
connect_timeout = 60
network_timeout = 60
charset = GBK
http.tracker_http_port = 8080
tracker_server = 120.78.198.32:22122
  • connect_timeout:连接超时时间,单位为秒
  • network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败
  • charset: 字符集
  • http.tracker_http_port :.tracker的http端口

3、创建核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spring:
servlet:
multipart:
max-request-size: 10MB
max-file-size: 10MB
application:
name: service-file
cloud:
nacos:
discovery:
server-addr: 120.78.198.32:8848
profiles:
active: dev
server:
port: 8100
# 开启feign对sentinel 的支持
feign:
sentinel:
enabled: true
swagger2:
base-package: com.hzx.mall.file.controller
name: 蔡大头
url: https://sutianxin.top
email: 763882220@qq.com
title: 畅购商城商品微服务
description: 文件管理微服务后端接口
version: 1.0
terms-of-service-url: https://sutianxin.top

2.2、文件信息实体类

创建一个实体类,用于封装文件上传信息。该实体类包括文件创建时间,文件作者,文件类型,文件大小和文件附加信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FastDfsFileInfo implements Serializable {
/**
* 文件名
*/
private String name;
/**
* 文件内容
*/
private byte[] content;
/**
* 文件扩展名,如 jpg,png
*/
private String ext;
/**
* 文件md5摘要值
*/
private String md5;
/**
* 文件作者
*/
private String author;


public FastDfsFileInfo(String name, byte[] content, String ext) {
this.name = name;
this.content = content;
this.ext = ext;
}
}

2.3、创建一个工具类,实现文件管理

此工具类实现文件上传、文件下载、文件删除、文件信息获取、Storage 和 Tracker 信息获取

1、使用一个静态代码块加载 Tracker 连接信息

加载 resources 目录下的 fdfs_client.conf 配置文件

1
2
3
4
5
6
7
8
9
static {
// 1 获取 classpath 下的文件,使用 ClassPathResource 的 getPath 方法获取路径
String configFileLocation = new ClassPathResource("fdfs_client.conf").getPath();
try {
ClientGlobal.init(configFileLocation);
} catch (Exception e) {
e.printStackTrace();
}
}

2、编写文件上传方法

用于将文件上传到项目,封装在上一步创建的文件实体类中,文件上传方法接收这个实体类对象,然后进行文件上传

  • 1 创建一个 Tracker 访问的客户端对象 TrackerClient
  • 2 通过 TrackerClient 访问 TrackerServer 服务,获取连接信息
  • 3 通过 TrackerServer 的连接信息可以获取 Storage 的连接信息,创建 StorageClient 对象存储 Storage 的连接信息
  • 4 通过 StorageClient 访问 Storage ,实现文件上传

其中 upload_file 方法需要传入三个参数

  1. 上传文件的字节数组
  2. 文件扩展名
  3. 文件附加参数,其中第三个参数为一个 NameValuePair 对象数组,这个类的源码及属性如下

可以看到这个类包括附加参数名及参数值,类似 K-V 键值对

1
2
3
4
5
public class NameValuePair {
protected String name;
protected String value;
...
}

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 文件上传
* @param fileInfo 待上传文件对象
*/
public void upload(FastDfsFileInfo fileInfo) throws Exception {
// 构造附加参数
NameValuePair[] additionalParameters = new NameValuePair[1];
additionalParameters[0] = new NameValuePair("author", fileInfo.getAuthor());

//1 创建一个 Tracker 访问的客户端对象 TrackerClient
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 访问 TrackerServer 服务,获取连接信息
TrackerServer trackerServer = trackerClient.getConnection();
//3 通过 TrackerServer 的连接信息可以获取 Storage 的连接信息,创建 StorageClient 对象存储 Storage 的连接信息
StorageClient storageClient = new StorageClient(trackerServer, null);
//4 通过 StorageClient 访问 Storage ,实现文件上传
/*
upload_file 方法参数说明:
1. 上传文件的字节数组
2. 文件扩展名,如 jpg
3. 文件附加参数,如拍摄地址,作者等
*/

storageClient.upload_file(fileInfo.getContent(), fileInfo.getExt(), additionalParameters);

}

StorageClient 对象的 upload_file 方法返回一个 String[] 数组,其中数组第一个元素是文件上传所存储的 storage 的组名字,如 group1

数组第二个元素为文件存储到 storage 上的文件名

  • 修改工具类中的文件上传方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 文件上传
* @param fileInfo 待上传文件对象
* @return 文件在fastDFS中的存储信息,为一个String数组
*/
public static String[] upload(FastDfsFileInfo fileInfo) throws Exception {
// 构造附加参数
NameValuePair[] additionalParameters = new NameValuePair[1];
additionalParameters[0] = new NameValuePair("author", fileInfo.getAuthor());

//1 创建一个 Tracker 访问的客户端对象 TrackerClient
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 访问 TrackerServer 服务,获取连接信息
TrackerServer trackerServer = trackerClient.getConnection();
//3 通过 TrackerServer 的连接信息可以获取 Storage 的连接信息,创建 StorageClient 对象存储 Storage 的连接信息
StorageClient storageClient = new StorageClient(trackerServer, null);
//4 通过 StorageClient 访问 Storage ,实现文件上传
/*
upload_file 方法参数说明:
1. 上传文件的字节数组
2. 文件扩展名,如 jpg
3. 文件附加参数,如拍摄地址,作者等
*/
return storageClient.upload_file(fileInfo.getContent(), fileInfo.getExt(), additionalParameters);
}
  • 编写一个控制器方法,用于上传文件

图片访问地址为:http://虚拟机ip:8080/组名/文件路径文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@CrossOrigin
@RestController
@RequestMapping("upload")
@Api(tags = "文件上传控制器")
public class FileUploadController {

@PostMapping
@ApiOperation("文件上传")
public Result upload(@RequestParam("file")MultipartFile file) throws Exception {
//1 封装文件信息
FastDfsFileInfo fileInfo = new FastDfsFileInfo(
file.getOriginalFilename(),
file.getBytes(),
StringUtils.getFilenameExtension(file.getOriginalFilename()),
null,
"蔡大头"
);
//2 调用工具类的文件上传方法上传文件
String[] uploadInfo = FastDfsUtil.upload(fileInfo);
//3 拼接文件的访问地址
String url = FastDfsConstant.UPLOAD_FILE_PREFIX + uploadInfo[0] + "/" + uploadInfo[1];
return Result.SUCCESS().message("上传成功!").data(url);
}
}
  • 测试文件上传接口

image-20210615211128275

  • 查看 url

image-20210615211211908

3、编写文件信息获取方法

使用 StorageClientget_file_info 方法获取,这个方法接收两个参数,一为文件组名,二为文件存储路径名

  • 1 创建 TrackerClient 对象,通过 TrackerClient 访问 TrackerServer
  • 2 通过 TrackerClient 对象获取 TrackerServer 连接对象
  • 3 通过 TrackerServer 获取 Storage 信息,创建 StorageClient 对象存储 Storage 信息
  • 4 通过 StorageClient 对象获取文件信息

该方法返回一个 FileInfo 对象,这个类的源码如下

1
2
3
4
5
6
7
public class FileInfo {
protected String source_ip_addr; // 文件 ip
protected long file_size; // 文件大小
protected Date create_timestamp; // 文件创建时间
protected int crc32;// 文件签名
//省略getter、setter、构造函数等
}

工具类中获取文件信息的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 获取文件信息
* @param groupName 文件的组名,如 group1
* @param remoteFileName 文件存储路径名,如 M00/00/00/XXXX.jpg
* @return 文件信息对象,包含文件ip、文件大小、文件创建时间及文件签名
*/
public static FileInfo getFileInfo(String groupName, String remoteFileName) throws Exception {
//1 创建 TrackerClient 对象,通过 TrackerClient 访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 对象获取 TrackerServer 连接对象
TrackerServer trackerServer = trackerClient.getConnection();
//3 通过 TrackerServer 获取 Storage 信息,创建 StorageClient 对象存储 Storage 信息
StorageClient storageClient = new StorageClient(trackerServer, null);
//4 通过 StorageClient 对象获取文件信息
return storageClient.get_file_info(groupName, remoteFileName);
}

4、抽取静态方法 initStorageClient

这个方法用于获取 StorageClient 对象

1
2
3
4
5
6
7
8
public static StorageClient initStorageClient() throws Exception {
//1 创建 TrackerClient 对象,通过 TrackerClient 访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 对象获取 TrackerServer 连接对象
TrackerServer trackerServer = trackerClient.getConnection();
//3 通过 TrackerServer 获取 Storage 信息,创建 StorageClient 对象存储 Storage 信息
return new StorageClient(trackerServer, null);
}

5、文件下载方法

该方法接收两个参数,一为文件组名,二为文件存储路径名,返回一个文件输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 文件下载方法
* @param groupName 文件的组名,如 group1
* @param remoteFileName 文件存储路径名,如 M00/00/00/XXXX.jpg
* @return 字节数组文件输入流
*/
public static InputStream download(String groupName, String remoteFileName) throws Exception {
//1 使用静态方法获取 StorageClient 对象
StorageClient storageClient = initStorageClient();
//2 使用 StorageClient 对象的 download_file 方法下载文件,这个方法返回一个 byte 数组
byte[] fileData = storageClient.download_file(groupName, remoteFileName);
return new ByteArrayInputStream(fileData);
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
InputStream is = download("group1","M00/00/00/rBIkcGDIpryAceEzAAYY1DtRiVA008.gif");
// 将文件输入流中的数据写入磁盘
FileOutputStream os = new FileOutputStream("G:\\Desktop\\renren\\1.gif");
// 定义一个缓冲区
byte[] buffer = new byte[1024];
while (is.read(buffer) != -1) {
os.write(buffer);
}
os.flush();
os.close();
is.close();
}

查看结果

image-20210615220903788

6、文件删除方法

通过 StorageClient 对象的 delete_file 方法进行删除,该方法接收两个参数,一为文件组名,二为文件存储路径名,当返回值为0时删除成功,不为0时删除失败,查看 delete_file 方法的源码

image-20210615222228412

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 文件删除方法
* @param groupName 文件的组名,如 group1
* @param remoteFileName 文件存储路径名,如 M00/00/00/XXXX.jpg
* @return 是否删除成功
*/
public static boolean removeFile(String groupName, String remoteFileName) throws Exception {
//1 获取 StorageClient 对象
StorageClient storageClient = initStorageClient();
//2 使用 StorageClient 对象的 delete_file 方法删除
return storageClient.delete_file(groupName, remoteFileName) == 0;
}
  • 测试,清除浏览器缓存后再次访问发现报 404

image-20210615222257170

7、禁用 Nginx 的缓存

  • 进入 storage 容器中
1
docker exec -it storage /bin/bash
  • 进入 /etc/nginx/conf 目录中修改配置文件
1
cd /etc/nginx/conf/vim nginx.conf
  • 添加配置

nginx.conf 中添加配置

1
add_header Cache-Control no-store;

image-20210615224645405

  • 退出并重启 storage 容器

  • 测试

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
if (removeFile("group1","M00/00/00/rBIkcGDIvauAeTxeAAPDZ5TkoVo915.jpg")) {
System.out.println("删除成功!");
} else {
System.out.println("删除失败!");
}
}

运行并重新刷新图片地址

image-20210615224910738

8、获取 Storage 信息

通过 TrackerClient 获取 Storage 信息

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取 StorageServer 对象
*/
public static StorageServer getStorageInfo() throws Exception {
//1 创建 TrackerClient 对象,通过 TrackerClient 访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 对象获取 TrackerServer 连接对象
TrackerServer trackerServer = trackerClient.getConnection();
//3 通过 TrackerClient 对象的 getStoreStorage 方法获取,需要传入 TrackerServer 对象
return trackerClient.getStoreStorage(trackerServer);
}

9、获取 Storage 组的 IP 和端口

1
2
3
4
5
6
7
8
9
10
11
/**
* @param groupName 文件的组名,如 group1
* @param remoteFileName 文件存储路径名,如 M00/00/00/XXXX.jpg
*/
public static ServerInfo[] getServerInfo(String groupName, String remoteFileName) throws Exception {
//1 创建 TrackerClient 对象,通过 TrackerClient 访问 TrackerServer
TrackerClient trackerClient = new TrackerClient();
//2 通过 TrackerClient 对象获取 TrackerServer 连接对象
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer,groupName,remoteFileName);
}