第3章 分布式文件存储FastDFS
1. FastDFS介绍
1.1 FastDFS简介
FastDFS是一个以C语言开发的一项开源轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
1.2 FastDFS体系结构
FastDFS 架构包括Tracker Server和Storage Server。客户端请求Tracker Server 进行文件上传、下载,通过Tracker Server调度最终由Storage Server完成文件上传和下载。
Tracker Server 作用是负载均衡和调度,通过Tracker Server在文件上传时可以根据一些策略找到Storage Server提供文件上传服务。可以将Tracker称为追踪服务器或调度服务器。
Storage Server作用是文件存储,负责文件上传、下载、修改和删除等功能。客户端上传的文件最终存储在Storage服务器上,Storage Server没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将Storage称为存储服务器。
1.3 上传流程
- Storage服务器定时向Tracker服务器发送上传信息
- 客服端向Tracker发送上传请求
- Tracker服务器查询可用的Storage
- Tracker服务器向客户端返回Storage服务器的ip和端口
- 客户端向Storage服务器上传文件
- Storage服务器生成file_id
- Storage服务器将上传内容写入磁盘
- Storage服务器向客户端返回file_id
- 客户端存储文件信息
其中file_id用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
1.4 FastDFS路径说明
group1/M00/00/00/rBGdZl5ak4yAHTNeAAxv6ziU5pA742.png
-
组名(/group1):文件上传后所在的Storage组名称。在文件上传成功后由Storage 服务器返回,需要客户端自行保存。
-
虚拟磁盘路径(/M00):Storage配置的虚拟路径,指向Storage组中某个节点的硬盘地址。与磁盘选项store_path*对应。如果配置了store_path0则是M00,如果配置了store_path1 则是M01,以此类推。
-
数据两级目录(/00/00):Storage服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据。该数值采用算法计算得出
-
文件名:与文件上传时的文件名称不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器IP地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
2. FastDFS搭建
2.1 安装FastDFS镜像
本文采用Docker容器进行FastDFS的环境搭建。
1、查询FastDFS镜像
docker search fastdfs
2、拉取镜像
docker pull morunchang/fastdfs
3、运行tracker
docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
4、运行storage
docker run -d --name storage --net=host -e TRACKER_IP=服务器IP:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
参数说明:
- -d 后台运行
- –name 别名。–name 空格 别名 --name=别名
- 使用的网络模式是–net=host, 告诉容器使用主机网络堆栈
- -e添加到环境变量中
- group1是组名,即Storage的组
- 如果想要增加新的Storage服务器,再次运行该命令,注意更换新组名
2.2 修改配置
1、 进入storage中
docker exec -it storage /bin/bash
2.、配置nginx.conf
vi /etc/nginx/conf/nginx.conf
修改以下内容
server {
listen 9000;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location ~ /M00 {
root /data/fast_data/data;
ngx_fastdfs_module;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
3、配置storage.conf
vi /etc/fdfs/storage.conf
修改以下内容
# 创建storage时的ip
tracker_server=服务器IP:22122
#推荐与Nignx配置的端口相同
http.server_port=9000
4、退出容器
exit
5、重启storage容器
docker restart storage
6、查看启动的容器
docker ps -a
7、开启启动设置
docker update --restart=always tracker
docker update --restart=always storage
8、开放端口
使用阿里云进行部署时,需要在安全组中开放9000、23000、22122端口,否则会造成无法访问
3. 文件存储微服务
在thanksong-springcloud-provider
模块下创建thankson-provider-fastdfs
微服务,该服务主要用于实现文件上传、删除等功能。
3.1 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">
<parent>
<artifactId>thanksong-springcloud-provider</artifactId>
<groupId>com.thankson.springcloud</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>thankson-provider-fastdfs</artifactId>
<dependencies>
<!--FastFDS-->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.27.2</version>
</dependency>
</dependencies>
</project>
3.2 application.yml配置
在resources文件夹下创建application.yml
server:
port: 9000
spring:
application:
name: FastDFS
servlet:
multipart:
max-request-size: 10MB
max-file-size: 10MB
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://www.xiexun.top:3306/changgou_sys?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
prefer-ip-address: true
fdfs:
# 读取时间
so-timeout: 1000
# 连接超时时间
connect-timeout: 60
# 缩略图
thumbImage:
# 宽
width: 150
# 高
height: 150
# tracker列表
tracker-list: #TrackerList参数,支持多个
- www.xiexun.top:22122
com:
thankson:
springcloud:
FastDFS:
pathPrefix: www.xiexun.top:9000
3.3 启动类
在com.thankson.fastdfs
包下创建启动类FastDFSApplication.java
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.thankson.fastdfs.dao"})
@ComponentScan(value = {"com.thankson.common.util","com.thankson.fastdfs"})
public class FastDFSApplication {
public static void main(String[] args) {
SpringApplication.run(FastDFSApplication.class, args);
}
}
3.4 代码编写
3.4.1 实体类
在com.thankson.fastdfs.pojo
包下创建FileMessage实体类
@Table(name = "tb_file_message")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FileMessage {
/**
* 自增ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
/**
* 文件名称
*/
@Column(name = "file_name")
private String fileName;
/**
* 文件类型
*/
@Column(name = "file_type")
@NotBlank(message = "文件类型不能为空")
private String fileType;
/**
* 文件扩展名
*/
@Column(name = "file_extension")
private String fileExtension;
/**
* 文件大小
*/
@Column(name = "file_size")
private Long fileSize;
/**
* 文件路径
*/
@Column(name = "file_path")
private String filePath;
/**
* 文件描述
*/
@Column(name = "description")
@NotBlank(message = "文件描述不能为空")
private String description;
/**
* 归属系统标识
*/
@Column(name = "sys_flag")
@NotBlank(message = "系统标识不能为空")
private String sysFlag;
/**
* 上传时间
*/
@Column(name = "create_date")
private Date createDate;
/**
* 上传人
*/
@Column(name = "uploader")
private String uploader;
/**
* 删除标识
*/
@Column(name = "del_flag")
private String delFlag;
}
3.4.2 数据访问层
在com.thankson.fastdfs.dao
包下创建FileMessageDao接口。
该接口需要继承tk.mybatis.mapper.common.Mapper类
public interface FileMessageDao extends Mapper<FileMessage> {
}
3.4.3 业务层
1、业务层接口
在com.thankson.fastdfs.service
包下创建FileService接口
public interface FileService {
/**
* 上传文件至FastDFS
*
* @param fileMessage 文件信息
* @author Thankson
* @date 2020年5月5日
*/
Result<Object> uploadFile(InputStream inputStream, FileMessage fileMessage);
}
2、业务层实现类
在com.thankson.fastdfs.service.impl
包下创建FileServiceImpl类
@Service
public class FileServiceImpl implements FileService {
@Autowired
private FileMessageDao fileMessageDao;
@Autowired
private FastFileStorageClient fastFileStorageClient;
@Value(value = "${com.thankson.springcloud.FastDFS.pathPrefix}")
private String pathPrefix;
@Override
public Result<Object> uploadFile(InputStream inputStream, FileMessage fileMessage) {
try {
//上传文件至FastDFS
StorePath storePath = fastFileStorageClient.uploadFile(inputStream, fileMessage.getFileSize(), fileMessage.getFileExtension(), null);
if (storePath == null) {
return new Result<>(false, 500, "文件上传失败");
}
//文件访问路径
String path = pathPrefix + "/" + storePath.getFullPath();
fileMessage.setFilePath(path);
//保存文件信息
int i = fileMessageDao.insertSelective(fileMessage);
if (i < 1) {
return new Result<>(false, 500, "保存文件信息失败");
}
} catch (Exception e) {
e.printStackTrace();
return new Result<>(false, 500, "上传文件时发生异常");
}
return new Result<>(true, 200, "上传成功", fileMessage);
}
}
3.4.4 控制层
在com.thankson.fastdfs.controller
包下创建FileController类
@RestController
@RequestMapping(value = "/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 上传文件
*
* @param file 文件
* @param fileMessage 文件信息
* @author Thankson
* @date 2020年5月5日
*/
@PostMapping(value = "/uploadFile")
public Result<Object> uploadFile(@RequestPart MultipartFile file, @RequestPart @Validated FileMessage fileMessage) {
fileMessage.setFileName(FilenameUtils.getBaseName(file.getOriginalFilename()));
fileMessage.setCreateDate(new Date());
fileMessage.setFileExtension(FilenameUtils.getExtension(file.getOriginalFilename()));
fileMessage.setFileSize(file.getSize());
fileMessage.setUploader("test");
InputStream inputStream;
try {
inputStream = file.getInputStream();
} catch (IOException e) {
e.printStackTrace();
return new Result<>(false, 500, "上传文件流获取失败");
}
return fileService.uploadFile(inputStream,fileMessage);
}
}
3.4.5 处理器
在thankson-common-util
工程com.thankson.common.util.handler
包下创建BaseExceptionHandler类
/**
* 异常处理
*
* @author Thankson
* @date 2020年5月5日
*/
@RestControllerAdvice
public class BaseExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 500, Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
}
@ExceptionHandler(ValidationException.class)
public Result<Object> handleValidationException(ValidationException e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 500, e.getCause().getMessage());
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Object> handleConstraintViolationException(ConstraintViolationException e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 500, e.getMessage());
}
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Object> handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DuplicateKeyException.class)
public Result<Object> handleDuplicateKeyException(DuplicateKeyException e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 500, "数据重复,请检查后提交");
}
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) {
logger.error(e.getMessage(), e);
return new Result<>(false, 500, "系统繁忙,请稍后再试");
}
}
3.5 测试
采用IDEA中自带的HTTP Client进行测试
3.5.1 创建FastDFS.http
3.5.2 添加请求
### 上传文件
POST http://localhost:9000/file/uploadFile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="新建文本文档.txt"
< C:\Users\69303\Desktop\新建文本文档.txt
--WebAppBoundary
Content-Disposition: form-data; name="fileMessage"
Content-Type: application/json
{
"author":"aaa",
"fileType": "1",
"description":"description",
"sysFlag":"test"
}
--WebAppBoundary--
3.5.3运行http后结果如下
3.5.4 访问链接
www.xiexun.top:9000/group1/M00/00/00/rBGdZl6xhfaAWrW6ABjs7y0zlQQ091.png
3.5.5 查看服务器
docker exec -it storage /bin/bash
ls /data/fast_data/data/00/00
4. 结束语
至此,FastDFS的上传功能已经完成。至于其他功能在后续使用时会进行开发