-------------------FastDFS---------------
1.FastDFS简介
为什么
1. 在分布式集群环境下,文件上传至节点A,这时通过负载均衡算法,访问到节点B,则不能访问到文件,这时会出现有时能访问有时不能访问的问题
2. 同时要考虑为文件做冗余备份、负载均衡、线性扩容等功能,这些都是单节点文件上传所不具备的
FastDFS体系结构
FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
上传流程
![](https://i-blog.csdnimg.cn/blog_migrate/7ee62b2617bf6e16075bf716dde29036.png)
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。
文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。
虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
2.FastDFS安装
我们使用Docker安装FastDFS
拉取镜像
docker pull morunchang/fastdfs
运行tracker
docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
运行storage
docker run -d --name storage --net=host -e TRACKER_IP=<your tracker server address>:22122 -e GROUP_NAME=<group name> morunchang/fastdfs sh storage.sh
例:
docker run -d --name storage --net=host -e TRACKER_IP=192.168.210.129:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
- 使用的网络模式是–net=host,host模式可以不用映射容器端口宿主机, 替换为你机器的Ip即可
- 是组名,即storage的组
- 如果想要增加新的storage服务器,再次运行该命令,注意更换 新组名
修改nginx的配置
进入storage的容器内部,修改nginx.conf
docker exec -it storage /bin/bash
进入后
vi /etc/nginx/conf/nginx.conf
添加以下内容(已经存在,不用改)
location ~ /M00 {
root /data/fast_data/data;
ngx_fastdfs_module;
}
禁止缓存:
add_header Cache-Control no-store;
(5)退出容器
exit
(6)重启storage容器
docker restart storage
查看tracker.conf和storage.conf配置文件
docker exec -it storage /bin/bash
cd /etc/fdfs
vim tracker.conf
vim storage.conf
3.文件上传微服务
搭建项目
创建文件上传微服务upload-service,通过fastdfs-client组件实现文件上传和删除的功能
(1)在spring cloud项目中创建子工程,修改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>springCloudDemo</artifactId>
<groupId>com.sh</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>upload-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- FastDFS依赖 -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
</dependency>
</dependencies>
</project>
(2)application.yml
server:
port: 9004
logging:
#file: demo.log
pattern:
console: "%d - %msg%n"
level:
org.springframework.web: debug
com.lxs: debug
spring:
application:
name: upload-service
servlet:
multipart:
enabled: true #开启文件上传
max-file-size: 10MB #单个文件上传大小
max-request-size: 20MB #总文件上传大小
fdfs:
# 链接超时
connect-timeout: 60
# 读取时间,超出则失败
so-timeout: 60
# 生成缩略图参数
thumb-image:
width: 150
height: 150
tracker-list: 192.168.210.129:22122
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
#更倾向于使用ip地址,而不是主机名
prefer-ip-address: true
#ip地址
ip-address: 127.0.0.1
#续约间隔,默认30秒
lease-renewal-interval-in-seconds: 5
#服务的实效时间, 默认90秒
lease-expiration-duration-in-seconds: 5
(3)启动类UploadApplication
package com.sh.upload;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //开启Eureka客户端发现功能
public class UploadApplication {
public static void main(String[] args) {
SpringApplication.run(UploadApplication.class, args);
}
}
(4) FastDfs配置类DfsConfig
package com.sh.upload.config;
import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(FdfsClientConfig.class)
public class DfsConfig {
}
工具类FileDfsUtil
调用fastdfs-client工具方法实现文件上传和删除
package com.sh.upload.config;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* @author hao
* @create 2020/12/10
* 书读百遍,其意自见
*/
@Component
public class FileDfsUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(FileDfsUtil.class);
@Autowired
private FastFileStorageClient storageClient ;
/**
* 上传文件
* @param multipartFile
* @return
* @throws Exception
*/
public String upload(MultipartFile multipartFile) throws Exception {
//方法一
//String extName = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
//方法二
String extName = multipartFile.getOriginalFilename().substring(
multipartFile.getOriginalFilename().
lastIndexOf(".") + 1);
StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
multipartFile.getInputStream(),
multipartFile.getSize(), extName, null);
return storePath.getFullPath() ;// "/group1/M00/OO/00/xxx.jpg"
}
/**
* 删除文件
* @param fileUrl
*/
public void deleteFile(String fileUrl) {
if (StringUtils.isEmpty(fileUrl)) {
LOGGER.info("fileUrl == >>文件路径为空...");
return;
}
try {
StorePath storePath = StorePath.parseFromUrl(fileUrl);
storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
} catch (Exception e) {
LOGGER.info(e.getMessage());
}
}
}
FileController
创建文件上传和删除功能的controller,实现文件删除
package com.sh.upload.controller;
import com.sh.upload.config.FileDfsUtil;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* @author hao
* @create 2020/12/11
* 书读百遍,其意自见
*/
@RestController
public class FileController {
@Autowired
private FileDfsUtil fileDfsUtil ;
@RequestMapping(value = "/hello",method = RequestMethod.POST)
public String hello(String s){
return s;
}
/**
* http://localhost:7010/swagger-ui.html
* http://192.168.72.130/group1/M00/00/00/wKhIgl0n4AKABxQEABhlMYw_3Lo825.png
*/
@RequestMapping(value = "/uploadFile", headers="content-type=multipart/form-data",method = RequestMethod.POST)
public ResponseEntity<String> uploadFile (@RequestParam("file") MultipartFile file){
String result ;
try{
String path = fileDfsUtil.upload(file) ;
if (!StringUtils.isEmpty(path)){
result = path ;
} else {
result = "上传失败" ;
}
} catch (Exception e){
e.printStackTrace() ;
result = "服务异常" ;
}
//new ResponseEntity<String>("错误请求", HttpStatus.BAD_REQUEST);400,"错误请求"
return ResponseEntity.ok(result);//status=200,内容显示result的值,
}
/**
* 文件删除
* @param filePathName
* @return
*/
@RequestMapping(value = "/deleteByPath", method = RequestMethod.GET)
public ResponseEntity<String> deleteByPath (String filePathName){
// String filePathName = "group1/M00/00/00/wKhjZF3WEDmAPSglAABSZAhj0eU111.jpg" ;
fileDfsUtil.deleteFile(filePathName);
return ResponseEntity.ok("SUCCESS") ;
}
}
postman测试
上传测试
在虚拟机中查看上传的图片
docker exec -it storage /bin/bash
cd /data/fast_data/data
ls
删除测试
----------------Swagger-------------
简介
Swagger是一款目前世界最流行的API管理工具。目前Swagger已经形成一个生态圈,能够管理API的整个生命周期,从设计、文档到测试与部署。Swagger有几个重要特性:
- 代码侵入式注解
- 遵循YAML文档格式
- 非常适合三端(PC、iOS及Android)的API管理,尤其适合前后端完全分离的架构模式。
- 减少没有必要的文档,符合敏捷开发理念
- 功能强大
作用
- 接口的文档在线自动生成
- 功能测试
优点
- 1. 大大减少前后端的沟通
- 2. 方便查找和测试接口
- 3. 提高团队的开发效率
- 4. 方便新人了解项目
配置swagger
引入依赖
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
配置类
package com.sh.upload.config;
/**
* @author hao
* @create 2020/12/13
* 书读百遍,其意自见
*/
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* Swagger 配置文件
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.sh.upload")) //swagger搜索的包
.paths(PathSelectors.any()) //swagger路径匹配
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("文件上传文档")
.description("使用FastDfs文件上传")
.version("version 1.0")
.build();
}
}
常用注解
swagger通过在controller中,声明注解,API文档进行说明
1、@Api():
- 用在请求的类上,表示对类的说明,也代表了这个类是swagger2的资源
参数:
- tags:说明该类的作用,参数是个数组,可以填多个。
- value="该参数没什么意义,在UI界面上不显示,所以不用配置"
- description = "用户基本信息操作"
2、@ApiOperation():
- 用于方法,表示一个http请求访问该方法的操作
参数:
- value="方法的用途和作用"
- notes="方法的注意事项和备注"
- tags:说明该方法的作用,参数是个数组,可以填多个。
- 格式:tags={"作用1","作用2"}
- (在这里建议不使用这个参数,会使界面看上去有点乱,前两个常用)
3、@ApiModel():
- 用于响应实体类上,用于说明实体作用
参数:
4、@ApiModelProperty:
- 用在属性上,描述实体类的属性
参数:
- description="描述实体的作用"
5、@ApiImplicitParams:
- 用在请求的方法上,包含多@ApiImplicitParam
6、@ApiImplicitParam:
- 用于方法,表示单独的请求参数
参数:
- name="参数ming"
- value="参数说明"
- dataType="数据类型"
- paramType="query" 表示参数放在哪里
- · header 请求参数的获取:@RequestHeader
- · query 请求参数的获取:@RequestParam
- · path(用于restful接口) 请求参数的获取:@PathVariable
- · body(不常用)
- · form(不常用)
- defaultValue="参数的默认值"
- required="true" 表示参数是否必须传
7、@ApiParam():
- 用于方法,参数,字段说明 表示对参数的要求和说明
参数:
- name="参数名称"
- value="参数的简要说明"
- defaultValue="参数默认值"
- required="true" 表示属性是否必填,默认为false
8、@ApiResponses:
- 用于请求的方法上,根据响应码表示不同响应
- 一个@ApiResponses包含多个@ApiResponse
9、@ApiResponse:
- 用在请求的方法上,表示不同的响应
参数
- code="404" 表示响应码(int型),可自定义
- message="状态码对应的响应信息"
10、@ApiIgnore():
- 用于类或者方法上,不被显示在页面上
使用
实体类
@ApiModel("用户对象模型")
public class User {
@ApiModelProperty("用户id")
private Long id;
private String username;
private String password;
private String email;
}
controller
package com.sh.upload.controller;
/**
* @author hao
* @create 2020/12/13
* 书读百遍,其意自见
*/
import com.sh.upload.domain.User;
import io.swagger.annotations.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
/**
* /user post 新增
* /{id} delete 删除
* /{id} put 更新
* /{id} get 根据id加载
* /list-page post 分页查询
*/
@RestController
@RequestMapping("/user")
@Api(tags = {"用户管理API"})
public class UserController {
@PostMapping
@ApiOperation(value = "新增用户", notes = "新增后返回当前用户")
@ApiResponses({
@ApiResponse(code = 200, message = "返回成功", response = User.class),
@ApiResponse(code = 400, message = "参数没有填好(id==1)", response = User.class),
@ApiResponse(code = 401, message = "权限不足(id==2)", response = User.class),
})
public ResponseEntity<User> add(User user) {
if (user.getId() == 1) {
return new ResponseEntity<>(user, HttpStatus.BAD_REQUEST); //400
} else if (user.getId() == 2) {
return new ResponseEntity<>(user, HttpStatus.UNAUTHORIZED); //401
} else {
return ResponseEntity.ok(user);
}
}
@PutMapping
@ApiOperation(value = "修改用户", notes = "修改后返回当前用户")
@ApiResponses({
@ApiResponse(code = 200, message = "返回成功", response = User.class),
@ApiResponse(code = 400, message = "参数没有填好(id==1)", response = User.class),
@ApiResponse(code = 401, message = "权限不足(id==2)", response = User.class),
})
public ResponseEntity<User> update(User user) {
if (user.getId() == 1) {
return new ResponseEntity<>(user, HttpStatus.BAD_REQUEST); //400
} else if (user.getId() == 2) {
return new ResponseEntity<>(user, HttpStatus.UNAUTHORIZED); //401
} else {
return ResponseEntity.ok(user);
}
}
@DeleteMapping("/{id}")
@ApiOperation(value = "删除用户", notes = "删除后返回当前id")
@ApiResponses({
@ApiResponse(code = 200, message = "返回成功", response = Long.class),
@ApiResponse(code = 400, message = "参数没有填好(id==1)", response = Long.class),
@ApiResponse(code = 401, message = "权限不足(id==2)", response = Long.class),
})
@ApiImplicitParam(paramType = "path", name = "id", value = "用户主键ID", required = true)
public ResponseEntity<Long> delete(@PathVariable Long id) {
if (id == 1) {
return new ResponseEntity<>(id, HttpStatus.BAD_REQUEST); //400
} else if (id == 2) {
return new ResponseEntity<>(id, HttpStatus.UNAUTHORIZED); //401
} else {
return ResponseEntity.ok(id);
}
}
@GetMapping("/{id}")
@ApiIgnore
public ResponseEntity<Long> toUpdate(@PathVariable Long id) {
if (id == 1) {
return new ResponseEntity<>(id, HttpStatus.BAD_REQUEST); //400
} else if (id == 2) {
return new ResponseEntity<>(id, HttpStatus.UNAUTHORIZED); //401
} else {
return ResponseEntity.ok(id);
}
}
@PostMapping("/list-page")
@ApiOperation(value = "分页查询", notes = "得到分页查询对象pageInfo")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "query", name = "pageNum", value = "当前页", required = false, defaultValue = "1"),
@ApiImplicitParam(paramType = "query", name = "pageSize", value = "每页行数", required = false, defaultValue = "10")
})
public ResponseEntity<String> findByPage(@RequestParam(defaultValue = "1", required = false) Integer pageNum, @RequestParam(defaultValue = "10", required = false) Integer pageSize) {
return ResponseEntity.ok("find page result...");
}
}
使用swagger测试文件上传