微服务商城系统(三) 分布式文件存储 FastDFS

一、 分布式文件存储 FastDFS

1、简介

     FastDFS 是一个 开源的 轻量级 分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。 特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
     FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容 (解决了 Spring MVC 上传文件时,容量不够用的问题)等机制,并注重高可用、高性能等指标,使用 FastDFS 很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
在这里插入图片描述

     FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度,最终由 Storage server 完成文件上传和下载。
     Tracker server 作用是负载均衡和调度(相当于是个控制中心 、 注册中心),通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务,可以将 tracker 称为追踪服务器或调度服务器。 Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将 storage 称为存储服务器。
    

2、工作原理

     我们现在要在微服务商城系统中使用 FastDFS,来实现文件的管理,流程大概是这样的:在这里插入图片描述

     可以看到,Storage 会定时注册,每过一段时间会将自己的信息发送给 Tracker,和 Tracker 保持 “心跳沟通” 。用 changgou-service-file 模块来做文件管理,通过控制层访问路径 /upload,用户上传文件时,那么程序就会到 Tracker 控制中心找返回的可用的 Storage,进行上传文件。图中还提到了 “同步备份” 和 “集群”,集群是 一组一组的,比如图中的三个组 Group 合起来,是一个集群,当容量不够时,可以添加新的 Gruop,这就是上文提到的 “线性扩容”,也叫水平扩容;而每个组里的是同步节点(不是 “主从” 喔)。
     再来看看这样的情况:比如说,第一次上传的时候,上传到了 192…12 这台机器上,那么这时 192…12 机器会立刻将文件同步到 192…13 机器上,这叫 “冗余备份”;用户下次再上传文件,如果 192…13 机器是空闲的,就会把 192…13 机器的 IP 地址和端口号返回给程序。同 Group 的机器里的数据总是一致的,如果某一台出故障了,也可以去其他机器上找数据,这就是 “容灾”。
    

3、文件上传流程

在这里插入图片描述

     图中第 7 步里的 “磁盘” 指的是服务器。
     客户端上传文件后,存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。比如:

group1/M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg
  • 组名:文件上传后所在的 storage 组名称,在文件上传成功后,有 storage 服务器返回,需要客户端自行保存。
  • 虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项 store_path* 对应,也就是说,指向了 Storage 组中某个节点的硬盘地址。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
  • 数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
  • 文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
4、框架搭建

     首先,需要拉取镜像( docker pull morunchang/fastdfs ) ,在虚拟机中查看,可以看到,已经下载镜像了。
在这里插入图片描述
     接下来,使用 docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh 运行 tracker;然后使用 docker run -d --name storage --net=host -e TRACKER_IP=192.168.211.132:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh 运行 storage :
在这里插入图片描述
参数说明:

  • docker run -d :后台运行。
  • 使用的网络模式是 –net=host, 192.168.211.132 是宿主机的IP
  • group1 是组名,即 storage 的组,如果想要增加新的 storage 服务器,再次运行该命令,注意更换新组名。
        
    运行结果:
    在这里插入图片描述
    可以看到,tracker 和 storage 启动成功了。

接下来,设置 restart 属性,设置开机自动启动:
在这里插入图片描述
在 nginx 的配置文件中:
在这里插入图片描述
     可以看到,以 /M00 开头的请求都会去找 ngx_fastdfs_module 模块。

     接下来,开始搭建文件微服务。
先在 changgou-service 中新建 module,名为 changgou-service-file ,导入依赖:

  	  <dependencies>
        <dependency>
            <groupId>net.oschina.zcx7878</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

在 resources 文件夹下创建 fastDFS 的配置文件 fdfs_client.conf:

connect_timeout=60
network_timeout=60
charset=UTF-8
http.tracker_http_port=8080
tracker_server=192.168.211.132:22122

参数说明:

  • connect_timeout:连接超时时间,单位为秒。
  • network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败
  • charset: 字符集
  • http.tracker_http_port :.tracker 的 http 端口
  • tracker_server: tracker 服务器 IP 和端口设置
        

提供 application.yml :

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
  application:
    name: file
server:
  port: 18082
eureka:
  client:
    service-url: 
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

     其中,max-file-size 是单个文件大小,max-request-size 是请求数据的大小(比如说,除了文件,还有表单)。
    
提供启动类:

@SpringBootApplication
@EnableEurekaClient
public class FileApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileApplication.class,args);
    }
}

运行后报错了:
在这里插入图片描述
     Action 里说需要把数据库放置在 classpath 下,但是这个系统是不需要使用数据库的,所以需要对数据库自动配置加载的行为进行排除。对启动类上的注释进行修改:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

运行结果:
在这里插入图片描述

二、文件上传

     接下来,就参考上传流程,实现上传功能。
首先,对文件上传的信息进行封装:

public class FastDFSFile implements Serializable {
    // 文件名
    private String name;

    // 文件字节数组(内容)
    private byte[] content;

    // 文件扩展名,比如 jpg、png、gif......
    private String ext;

    // 文件 MD5 摘要值,为文件生成唯一值
    private String md5;

    // 上传者
    private String author;

/* 构造方法、setter、getter 方法省略*/

    
提供文件管理工具类:

public class FastDFSUtil {

    /* 加载 Tracker 连接信息 */
    static {
        try {

            // 查找 classpath 下的文件路径
            String filename = new ClassPathResource("fdfs_client.conf").getPath();

            // 初始化 Tracker 连接信息
            ClientGlobal.init(filename);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }

    /* 文件上传 */
    public static void upload(FastDFSFile file) throws IOException, MyException {

        // 附加参数
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] = new NameValuePair("附加参数", "附加参数值");

        // 创建一个 Tracker 访问的客户端对象 TrackerClient
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 访问 TrackerServer 服务,获取连接信息(包含 Storage)
        TrackerServer trackerServer = trackerClient.getConnection();

        // 创建 StorageClient 对象,存储 Storage 的连接信息
        StorageClient storageClient = new StorageClient(trackerServer, null);

        // 通过 StorageClient 访问 Storage,实现文件上传,并且获取文件上传后的存储信息
        // 传入的参数分别是 上传文件的字节数组、扩展名、附加参数
        storageClient.upload_file(file.getContent(), file.getExt(), meta_list);

    }
}

提供控制类:

@RestController
@RequestMapping
@CrossOrigin
public class FileUploadController {

    @PostMapping
    /* 文件上传 */
    public Result upload(@RequestParam(value = "file") MultipartFile file) throws IOException, MyException {
        FastDFSFile file1 = new FastDFSFile(
                // 文件名
                file.getName(),
                // 文件字节数组
                file.getBytes(),
                // 文件扩展名
                StringUtils.getFilenameExtension(file.getOriginalFilename())
        );

        // 调用文件上传工具类 将文件传入到 FastDFS 中
        FastDFSUtil.upload(file1);
        return new Result(true, StatusCode.OK, "上传成功");
    }
}

运行结果:
在这里插入图片描述
     文件管理工具类中使用到的方法 storageClient.upload_file(file.getContent(), file.getExt(), meta_list); 的返回值是 String[ ] 类型,即 String 数组,数组里有两个元素,分别是 :

  • [0] : 文件上传所存储的 Storage 组名
  • [1]:文件名
         比如上文举的例子 group1/M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg 中,组名是 group1,文件名是 M00/02/44/ wKsjiodfujfifhreuihkdszhfurie.jpg 。
         我们把 upload(FastDFSFile file) 方法的返回值改为 String[ ] ,接下来,我们把这个 String 数组取出来 ,放到响应结果中去,将控制类方法的代码修改为:
		// 返回值是 String 数组
         String[] filename=FastDFSUtil.upload(file1);
		// 拼接访问地址
        String url="http://192.168.211.132:8080/"+filename[0]+"/"+filename[1];
        return new Result(true, StatusCode.OK, "上传成功",url);

运行结果:
在这里插入图片描述
(🔍 访问这个路径,出现了问题,中文乱码了 😅 有待解决。)

     那么,上传的文件存到哪里去了呢 ❔ 在 Storage 里面,具体来看:
在虚拟机中启动 Storage ,并 vi 进入配置文件:
在这里插入图片描述
在这里插入图片描述
     可以看到,Storage 的通信端口号是 23000,heart_beat_interval=30 表示每 30 s 发送一次信息。而 Tracker 的通信端口号是 22122:
在这里插入图片描述

在这里插入图片描述

     还可以看到工作线程数。
在这里插入图片描述

store_path0 表示第一个虚拟路径,我们可以看看 /data/fast_data 目录:
在这里插入图片描述
上传文件时是会根据文件名计算哈希值的,值决定放置的目录。继续访问 00 目录 :
在这里插入图片描述
可以看到我们上传的文件。
    

三、文件信息获取

     我们已经知道,文件是上传到 Storage 里的,那么,要获取文件信息,我们需要先访问 Tracker,才能知道文件在哪个 Storage 里。

💎 代码:

 /* 获取文件信息
     * @param groupName 文件的组名,比如 group1
     * @param remoteFileName 文件存储路径名,比如 M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt
     * @return
     */
    public static void getFileInfo(String groupName, String remoteFileName) throws IOException, MyException {
        // 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        TrackerServer trackerServer = trackerClient.getConnection();

        //通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
        StorageClient storageClient = new StorageClient(trackerServer, null);

        // 获取文件信息
        FileInfo fileInfo = storageClient.get_file_info(groupName, remoteFileName);
    }

主方法:

public static void main(String[] args) throws IOException, MyException {
        FileInfo fileInfo = getFileInfo("group1", "M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt");
        System.out.println(fileInfo.getSourceIpAddr());
        System.out.println(fileInfo.getFileSize());
    }

运行结果:
在这里插入图片描述
    

四、文件下载

     文件是存在 Storage 里的,所以和以上步骤是类似的。
💎 代码:

 /* 文件下载*/
    public static InputStream downLoadFile(String groupName, String remoteFileName) throws IOException, MyException {
        // 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        TrackerServer trackerServer = trackerClient.getConnection();

        //通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
        StorageClient storageClient = new StorageClient(trackerServer, null);
        byte[] buffer = storageClient.download_file(groupName, remoteFileName);
        return new ByteArrayInputStream(buffer);
    }

接下来看文件下载效果,将下载的字节数组写到本地磁盘。在主方法中添加代码:

 // 文件下载
        InputStream is = downLoadFile("group1", "M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt");
        // 将文件写入本地磁盘
        FileOutputStream fos = new FileOutputStream("D:/1.txt");
        byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            fos.write(buffer);
        }
        fos.flush();
        fos.close();
        fos.close();

运行成功,在 D 盘看到了 1.txt。
    

五、文件删除

     文件是存在 Storage 里的,所以和以上步骤是类似的。
💎 代码:

/* 文件删除 */
    public static int deleteFile(String groupName, String remoteFileName) throws IOException, MyException {
        // 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        TrackerServer trackerServer = trackerClient.getConnection();

        //通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
        StorageClient storageClient = new StorageClient(trackerServer, null);
        return storageClient.delete_file(groupName, remoteFileName);
    }

在主方法里调用删除文件的方法,然后去看虚拟机,确实删除了:
在这里插入图片描述
     接下来去浏览器里访问http://192.168.211.132:8080/group1/M00/00/00/wKjThGAyCVCASJtTAAABPGy6xXQ466.txt,会发现还是访问到了文档 😅,但是清除了缓存,再去访问,就 404 啦,不过这样让用户手动清除缓存,是不科学的,应该对 Nginx 设置禁止缓存,在虚拟机中启动 Storage,然后进入 /etc/nginx/conf 目录:
在这里插入图片描述
vi 模式进入 nginx.conf 文件,对 Nginx 设置禁止缓存,输入 add_header Cache-Control no-store;
在这里插入图片描述
     再上传一遍文件(这时候注意到,生成的文件名是不同的),然后删除文件,再访问相同路径,就不需要手动清缓存,直接显示 404 啦。
    

六、获取 Storage、Tracker 信息

  • 获取 Storage 信息
         获取 Storage 信息。只需要访问 Traker,Traker 就会把 Storage 的信息返回啦。
        
    💎 代码:
/* 获取 Storage 信息*/
public static StorageServer getStorage() throws IOException {
    // 创建 TrackerClient 对象,访问 TrackerServer
    TrackerClient trackerClient = new TrackerClient();

    // 通过 TrackerClient 获取 TrackerServer 的连接对象,
    TrackerServer trackerServer = trackerClient.getConnection();

    // 获取 Storage 信息
    return trackerClient.getStoreStorage(trackerServer);
}

     在主方法中进行测试:

 		System.out.println(getStorage());
        System.out.println(getStorage().getStorePathIndex());
        System.out.println(getStorage().getInetSocketAddress().getHostString());

     运行结果:
在这里插入图片描述
     可以看到,下标是 0,IP 地址是 192.168.211.132
    

  • 根据 文件组名 和 文件存储路径 获取 Storage 组的 IP 和端口信息

     💎 代码:

	/* 获取 Storage IP、端口信息 */
    public static ServerInfo[] getStorage1(String groupName, String filename) throws IOException {
        // 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        TrackerServer trackerServer = trackerClient.getConnection();

        return trackerClient.getFetchStorages(trackerServer, groupName, filename);
    }

     这个方法是获取文件所在的 Storage 组的信息,在主方法里需要遍历组的每个 Storage 机器:

		// 获取 Storage 组的 IP和端口信息
        ServerInfo[] groups = getStorage1("group1",
                "M00/00/00/wKjThGAyVUiARmICAAABPGy6xXQ596.txt");
        for (ServerInfo group : groups) {
            System.out.println(group.getIpAddr());
            System.out.println(group.getPort());
        }

     运行结果:
在这里插入图片描述

  • 获取 Tracker 信息

     💎 代码:

/* 获取 Tracker 信息 */
public static String getTrackerInfo() throws IOException {
    // 创建 TrackerClient 对象,访问 TrackerServer
    TrackerClient trackerClient = new TrackerClient();

    // 通过 TrackerClient 获取 TrackerServer 的连接对象,
    TrackerServer trackerServer = trackerClient.getConnection();

    // Tracker IP、HTTP 端口
    int port = ClientGlobal.getG_tracker_http_port();
    String ip = trackerServer.getInetSocketAddress().getHostString();
    String url = "http://" + ip + ":" + port;
    return url;
}

     将方法返回值输出,运行结果:
在这里插入图片描述
     可以看到,和配置文件 fdfs_client.conf 里 IP 和 端口号是一致的,在这个项目里,Nginx 和 Tracker 端口号都是 8080,Nginx 的配置文件截图如下:
在这里插入图片描述

我们访问文件,其实是访问 Nginx,Nginx 会访问 Tracker,然后 Tracker 看文件在哪个 Storage 里,因为 Nginx 和 Tracker 端口号一样,所以在 文件管理工具类中,拼接的访问地址原先是 :

String url="http://192.168.211.132:8080/"+filename[0]+"/"+filename[1];

可以换成动态获取 IP 的形式 (注意只能用在 Nginx 和 Tracker 端口号 一致的情况喔) :

 String url= FastDFSUtil.getTrackerInfo() +"/" + filename[0] +"/"+ filename[1];

接下来,我们优化一下代码,把相同的部分提取出来,比如:

 public static TrackerServer getTrackerServer() throws IOException {
        // 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        return trackerClient.getConnection();
    }
🎉 补充:Classpath

classpath 通常被用来指定配置 或者 资源文件的路径:
在这里插入图片描述

七、总结

(1)FastDFS 是一个 开源的 轻量级 分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了 大容量存储 和 负载均衡 的问题。
     FastDFS 架构包括 Tracker server 和 Storage server。
     Tracker server 的作用是负载均衡和调度(相当于是个控制中心 、 注册中心),而 客户端上传的文件最终是存储在 Storage 服务器上(相当于干活的)。
     Storage 会定时注册,每过一段时间会将自己的信息发送给 Tracker,和 Tracker 保持 “心跳沟通” ♥ 。
    
(2)要读取 maven 项目 resource 下的文件,可以使用 Spring 的 ClassPathResource 类,构造方法的参数便是 resources 目录下的文件路径,这里使用的是相对路径(相对于resouces目录而言的),即 单纯的文件名即可。
    
(3)客户端上传文件后,存储服务器会将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。组名是 文件上传后所在的 storage 组名称;虚拟磁盘路径,与磁盘选项 store_path* 对应,也就是说,指向了 Storage 组中某个节点的硬盘地址。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推;文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
    
(4)文件的上传、下载、删除,过程都是:

  		// 创建 TrackerClient 对象,访问 TrackerServer
        TrackerClient trackerClient = new TrackerClient();

        // 通过 TrackerClient 获取 TrackerServer 的连接对象,
        TrackerServer trackerServer = trackerClient.getConnection();

        //通过 TrackerServer 获取到 Storage 信息,并创建 StorageClient 对象存储 Storage 信息
        StorageClient storageClient = new StorageClient(trackerServer, null);

然后调用 storageClient 的 upload_file、download_file、delete_file 。
    
(5)如果想要获取 Storage 信息,因为 Storage 会定时向 Tracker 进行注册,所以,只需要访问 Traker,调用 trackerClient.getStoreStorage(trackerServer) 方法即可,返回的是个 StorageServer 类对象,可以调用它的 getStorePathIndex() 方法知道下标,也可以调用 getInetSocketAddress 知道 socket 地址。
     如果想要根据 文件组名 和 文件存储路径 获取 Storage 组的 IP 和端口信息,
调用 trackerClient.getFetchStorages(trackerServer, groupName, filename) 即可,获取文件所在的 Storage 组的信息,因为是 “组”,所以需要遍历组的每个 Storage 机器。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值