在分布式系统中,文件存储是绕不开的核心需求 —— 单体架构下 “本地存文件” 的简单方案,在多服务部署时会面临文件不一致、存储容量瓶颈、访问性能不足等问题。而FastDFS作为轻量级分布式文件系统,凭借其高性价比、易部署、支持负载均衡与容量扩展的特性,成为中小团队解决海量文件存储的首选方案。

        本文基于系统学习笔记,从架构演进、原理剖析到实战落地,带你全面掌握 FastDFS,从 “知道是什么” 到 “能动手用”,再到 “理解为什么这么设计”。

目录

一、引言:为什么需要分布式文件存储?

二、从单体到分布式:文件管理的架构演进

2.1 单体架构的文件管理:简单但局限

2.2 分布式架构的文件管理:问题与解决方案

2.2.1 分布式文件管理的核心解决方案

三、分布式文件系统(DFS)概述

3.1 什么是分布式文件系统(DFS)?

3.2 常见 DFS 方案对比:该选哪一个?

四、FastDFS 核心解析:从简介到架构

4.1 FastDFS 简介:核心特点与适用场景

4.1.1 核心特点

4.1.2 适用场景

4.1.3 官方资源

4.2 FastDFS 架构:面试必懂的核心设计

4.2.1 架构图与核心角色

4.2.2 架构深层解读(面试重点)

五、实战:基于 Docker 快速部署 FastDFS

5.1 环境准备

5.2 步骤 1:拉取 FastDFS 镜像

5.3 步骤 2:创建 Tracker Server 容器

5.3.1 创建宿主机目录

5.3.2 启动 Tracker 容器

5.3.3 验证 Tracker 是否启动成功

5.4 步骤 3:创建 Storage Server 容器

5.4.1 创建宿主机目录

5.4.2 启动 Storage 容器

5.4.3 验证 Storage 是否启动成功

5.5 常见问题解决(实战必看)

5.5.1 重启 Storage 容器报错:pid 文件冲突

5.5.2 Storage 磁盘空间不足:无法上传文件

六、FastDFS 核心操作:代码实现上传、下载与删除

6.1 项目初始化:工程结构与依赖

6.1.1 工程结构

6.1.2 POM 依赖

6.2 数据库设计:存储文件元数据

6.2.1 建表语句

6.3 配置文件:连接 FastDFS 与数据库

6.3.1 FastDFS 配置(fdfs.properties)

6.3.2 应用配置(application.yml)

6.4 核心工具类:FastDFSUtils

6.5 业务层:Service 与 Mapper

6.5.1 实体类(MyFile.java)

6.5.2 Mapper 接口(MyFileMapper.java)

6.5.3 服务接口与实现(FileSystemService.java)

6.6 控制器:处理 HTTP 请求(FileSystemController.java)

6.7 页面实现:Thymeleaf 模板

6.7.1 首页(index.html):展示文件列表与操作

6.7.2 上传页(upload.html):文件上传表单

6.8 启动类(TestFdfsClient.java)

七、文件在线预览:Nginx 的关键作用

7.1 为什么需要 Nginx?

7.2 Nginx 核心配置解读

配置解读:

7.3 预览原理

八、总结与后续学习方向

8.1 FastDFS 核心优势回顾

8.2 后续学习方向


一、引言:为什么需要分布式文件存储?

在聊 FastDFS 之前,我们先搞清楚一个核心问题:为什么单体架构的文件管理方案会失效?

二、从单体到分布式:文件管理的架构演进

文件管理的方案必须适配整体系统架构,单体与分布式架构下的文件存储逻辑差异巨大,这也是我们引入 FastDFS 的根本原因。

2.1 单体架构的文件管理:简单但局限

单体架构中,应用服务(如 Tomcat)与文件资源存储在同一台服务器,客户端上传 / 下载文件直接操作本地磁盘:

  • 优势:部署简单、本地读写速度快,无需额外维护文件服务;
  • 痛点
    1. 文件过多会占用服务器磁盘空间,影响应用服务性能;
    2. 绝对路径 / 相对路径依赖服务器环境,迁移困难;
    3. 一旦服务器故障,文件与应用同时不可用,无容灾能力。

2.2 分布式架构的文件管理:问题与解决方案

分布式架构下,应用服务拆分为多个节点(如 Tomcat-1、Tomcat-2)部署,若沿用单体文件方案,会出现文件不一致的致命问题:

  • 场景举例:张三通过 Tomcat-1 上传文件 A,李四通过 Tomcat-2 下载文件 A 时,发现文件不存在;
  • 核心原因:文件仅存储在上传时的应用节点,其他节点无法访问。

2.2.1 分布式文件管理的核心解决方案

引入独立的文件服务器,集中管理所有应用节点的文件资源 —— 所有应用节点上传 / 下载文件时,均与统一的文件服务器交互,从根本上解决文件不一致问题。

而 FastDFS,就是这个 “专业的文件服务器” 的最佳选择之一。

三、分布式文件系统(DFS)概述

在深入 FastDFS 之前,我们先了解 “分布式文件系统” 的通用概念,以及常见方案的对比,帮你理解 FastDFS 的定位。

3.1 什么是分布式文件系统(DFS)?

分布式文件系统(Distributed File System,DFS)是指:文件的物理存储资源不局限于本地节点,而是通过网络分布在多个节点上,但对外呈现为一个统一的逻辑文件系统。

它的核心价值是:

  • 突破单节点存储容量限制,支持海量文件存储;
  • 实现负载均衡,提高文件读写性能;
  • 提供容灾能力,避免单点故障导致文件丢失。

3.2 常见 DFS 方案对比:该选哪一个?

市面上有多种分布式文件系统,各自适配不同场景,下表帮你快速选型:

方案核心特点适用场景局限性
FastDFS轻量级、纯 C 实现、支持分组备份、负载均衡中小团队、文件大小≤500MB(如图片、文档)不支持 POSIX 访问,仅通过 Client API 操作
GlusterFS高可扩展、模块化设计、支持多种卷类型大型集群、需要灵活扩展的场景对硬件 / 网络要求高,配置复杂
GoogleFS高性能、高可靠、支持 PB 级数据大型互联网公司、海量大数据处理不开源,仅内部使用(HDFS 借鉴其设计)
TFS淘宝开源、针对小文件(≤1MB)优化电商场景(如淘宝商品图片)对大文件支持差,维护成本高
阿里云 OSS云服务、免部署、高可用、按量付费不想维护自建服务、需要弹性扩展的场景依赖云厂商,长期使用成本较高

结论:若你是中小团队,需要自建文件服务,且主要存储图片、文档等中小文件,FastDFS 是性价比最高的选择

四、FastDFS 核心解析:从简介到架构

FastDFS 是由国人余庆(happy_fish100)开发的开源分布式文件系统,自 2008 年启动以来,已成为国内中小团队分布式文件存储的主流方案。

4.1 FastDFS 简介:核心特点与适用场景

4.1.1 核心特点
  • 轻量级:纯 C 实现,无依赖,部署简单,资源占用低;
  • 海量存储:支持存储服务器在线扩容,突破单节点容量限制;
  • 负载均衡:Tracker Server 实现调度,避免单点压力过大;
  • 容灾备份:Storage Server 分组存储,同组内节点文件完全同步;
  • 空间优化:相同内容的文件仅存储一份,节约磁盘空间;
  • 跨平台:支持 Linux、FreeBSD、AIX 等 UNIX 系统(不支持 Windows)。
4.1.2 适用场景
  • 中小网站 / 应用的资源存储(图片、文档、音频、视频);
  • 文件大小建议≤500MB(大文件推荐使用对象存储如 OSS);
  • 对存储成本敏感,希望通过廉价服务器搭建文件服务的场景。
4.1.3 官方资源

4.2 FastDFS 架构:面试必懂的核心设计

FastDFS 的架构极其简洁,仅包含三个核心角色,但却实现了负载均衡、容灾备份等复杂能力,是面试高频考点。

4.2.1 架构图与核心角色

FastDFS 的服务端仅包含两类节点,客户端通过 API 与服务端交互:

  1. Client(客户端)

    • 发起文件上传 / 下载 / 删除请求的应用(如 Java Spring Boot 项目);
    • 通过 Tracker Server 获取 Storage Server 地址,再与 Storage Server 交互文件。
  2. Tracker Server(跟踪服务器)

    • 核心作用:调度与负载均衡,不存储文件数据;
    • 维护内存中的集群状态:记录所有 Storage Server 的分组、IP、端口、磁盘使用情况;
    • 无主从关系:多个 Tracker 节点平等,通过客户端随机选择实现高可用;
    • 不与其他 Tracker 节点通信,仅接收 Storage 节点的状态上报。
  3. Storage Server(存储服务器)

    • 核心作用:存储文件数据与文件元数据(如文件名、大小、上传时间);
    • 分组管理(Group):Storage 节点按组划分,同组内节点文件完全同步(备份),不同组管理不同文件(扩容);
    • 目录结构:文件存储在/var/fdfs/data下,自动创建 256×256 的子目录(00~FF),避免单目录文件过多影响性能;
    • 主动上报状态:定期向 Tracker Server 上报磁盘使用、文件同步情况。
4.2.2 架构深层解读(面试重点)
  • 无主从设计:Tracker 与 Storage 均无主从,所有同角色节点平等,避免单点故障;
  • 分组备份:同组 Storage 节点文件实时同步,确保数据不丢失(如 Group1 有 2 个节点,上传到其中一个,另一个自动同步);
  • 跨组扩容:新增 Group(如 Group2)即可扩展存储容量,不同 Group 间无通信,独立管理;
  • 负载均衡:Client 上传文件时,Tracker 会选择空闲的 Storage 节点,避免某节点压力过大。

五、实战:基于 Docker 快速部署 FastDFS

手动编译安装 FastDFS 步骤繁琐(需依赖 libfastcommon 等库),而 Docker 镜像已封装好所有依赖,是快速上手的最佳方式。

5.1 环境准备

  • 操作系统:Linux(如 CentOS 7/8,Docker 不推荐 Windows);
  • 已安装 Docker:若未安装,可执行以下命令快速安装:
    # 安装Docker
    yum install -y docker
    # 启动Docker服务
    systemctl start docker
    # 设置Docker开机自启
    systemctl enable docker
    

5.2 步骤 1:拉取 FastDFS 镜像

使用社区维护的delron/fastdfs镜像(包含 Tracker、Storage 与 Nginx):

docker pull delron/fastdfs

5.3 步骤 2:创建 Tracker Server 容器

Tracker 负责调度,默认端口 22122,需映射宿主机目录保存日志与数据:

5.3.1 创建宿主机目录
# 用于存储Tracker的日志与数据
mkdir -p /opt/fdfs/tracker
5.3.2 启动 Tracker 容器
docker run -d \
  --network=host \  # 使用宿主机网络,避免端口映射复杂(容器端口直接用宿主机端口)
  --name tracker \  # 容器名
  -v /opt/fdfs/tracker:/var/fdfs \  # 目录映射:宿主机/opt/fdfs/tracker → 容器/var/fdfs
  delron/fastdfs \  # 镜像名
  tracker  # 启动Tracker服务
5.3.3 验证 Tracker 是否启动成功
# 查看容器日志
docker logs -f tracker
# 若看到"port=22122",说明启动成功

5.4 步骤 3:创建 Storage Server 容器

Storage 负责存储文件,默认端口 23000,需指定 Tracker 地址与所属分组:

5.4.1 创建宿主机目录
# 用于存储Storage的日志与文件数据
mkdir -p /opt/fdfs/storage
5.4.2 启动 Storage 容器
docker run -d \
  --network=host \  # 与Tracker同网络
  --name storage \  # 容器名
  -v /opt/fdfs/storage:/var/fdfs \  # 目录映射
  -e TRACKER_SERVER=192.168.163.132:22122 \  # 替换为你的宿主机IP+Tracker端口
  -e GROUP_NAME=group1 \  # 指定Storage所属分组(默认group1)
  delron/fastdfs \  # 镜像名
  storage  # 启动Storage服务
5.4.3 验证 Storage 是否启动成功
# 查看Storage日志
docker logs -f storage
# 若看到"successfully connect to tracker server",说明与Tracker通信成功

5.5 常见问题解决(实战必看)

5.5.1 重启 Storage 容器报错:pid 文件冲突

错误信息

tail: cannot open '/var/fdfs/logs/storaged.log' for reading: No such file or directory

原因:容器关闭时未删除/var/fdfs/data/*.pid文件,重启时认为进程已存在。解决方案:删除宿主机映射目录中的 pid 文件,再重启:

# 删除Tracker与Storage的pid文件
rm -rf /opt/fdfs/tracker/data/*.pid
rm -rf /opt/fdfs/storage/data/*.pid

# 重启容器(或编写脚本一键启动)
docker stop tracker storage
docker start tracker storage
5.5.2 Storage 磁盘空间不足:无法上传文件

原因:Tracker 默认配置 “剩余磁盘空间≤10% 时,禁止上传文件”,若 Linux 虚拟机磁盘过小(如 20GB),易触发此限制。解决方案:修改 Tracker 配置,降低磁盘空间限制:

# 进入Tracker容器
docker exec -it tracker bash

# 编辑Tracker配置文件
vi /etc/fdfs/tracker.conf

# 修改磁盘限制(如改为10KB,根据实际环境调整)
reserved_storage_space = 10K  # 默认10%,改为10K允许几乎满盘时仍可上传

# 退出容器
exit

# 重启Tracker(需先删除pid文件)
rm -rf /opt/fdfs/tracker/data/*.pid
docker restart tracker

六、FastDFS 核心操作:代码实现上传、下载与删除

我们基于 Spring Boot 搭建文件管理系统,实现文件的上传、下载、删除与列表展示,全程贴合企业实战场景。

上传文件流程图:

下载文件流程图:

删除文件流程图:

6.1 项目初始化:工程结构与依赖

6.1.1 工程结构
fastdfs_based_file_mgr/
├── src/
│   ├── main/
│   │   ├── java/com/bjsxt/
│   │   │   ├── controller/  # 控制器
│   │   │   ├── mapper/      # MyBatis Mapper
│   │   │   ├── pojo/        # 实体类
│   │   │   ├── service/     # 服务层
│   │   │   ├── util/        # 工具类(FastDFSUtils)
│   │   │   └── TestFdfsClient.java  # 启动类
│   │   └── resources/
│   │       ├── application.yml  # 应用配置
│   │       ├── fdfs.properties  # FastDFS配置
│   │       ├── mybatis/         # MyBatis XML(可选)
│   │       └── templates/       # Thymeleaf页面
│   └── test/  # 测试类
└── pom.xml  # 依赖配置
6.1.2 POM 依赖
<?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.bjsxt</groupId>
    <artifactId>fastdfs_based_file_mgr</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- Spring Boot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
    </parent>

    <dependencies>
        <!-- FastDFS Java客户端 -->
        <dependency>
            <groupId>cn.bestwu</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27</version>
        </dependency>
        <!-- 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <!-- Web支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Thymeleaf页面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Lombok简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

6.2 数据库设计:存储文件元数据

FastDFS 存储文件数据,数据库(MySQL)存储文件元数据(如原始文件名、分组、远程路径),方便查询与管理:

6.2.1 建表语句
CREATE TABLE `tb_files` (
  `id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
  `file_name` varchar(255) COMMENT '文件原始名称',
  `group_name` varchar(255) COMMENT 'FastDFS分组名(如group1)',
  `remote_name` varchar(255) COMMENT 'FastDFS生成的远程路径(如M00/00/00/wKgxgk5HbLvfP86RAAAAChd9X1Y736.jpg)',
  `length` bigint COMMENT '文件大小(单位:字节)',
  `create_time` datetime COMMENT '上传时间'
);

6.3 配置文件:连接 FastDFS 与数据库

6.3.1 FastDFS 配置(fdfs.properties)
# 连接超时时间(秒)
fastdfs.connect_timeout_in_seconds=10
# 网络超时时间(秒)
fastdfs.network_timeout_in_seconds=30
# 字符集
fastdfs.charset=UTF-8
# Tracker Server地址(多个用逗号分隔)
fastdfs.tracker_servers=192.168.137.128:22122  # 替换为你的Tracker IP+端口
6.3.2 应用配置(application.yml)
spring:
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/s118?serverTimezone=Asia/Shanghai&useSSL=false
    username: root  # 你的MySQL用户名
    password: root  # 你的MySQL密码
  # Thymeleaf配置(默认即可)
  thymeleaf:
    cache: false  # 开发时关闭缓存

# MyBatis配置
mybatis:
  type-aliases-package: com.bjsxt.pojo  # 实体类别名包
  mapper-locations: classpath:mybatis/*.xml  # Mapper XML路径(若用注解可省略)

6.4 核心工具类:FastDFSUtils

封装 FastDFS 的上传、下载、删除等操作,简化业务代码(核心是通过StorageClient与 FastDFS 交互):

package com.bjsxt.util;

import org.apache.commons.lang3.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;

/**
 * FastDFS工具类:封装上传、下载、删除等操作
 */
public final class FastDFSUtils {
    // 配置文件属性
    private final static Properties PROPERTIES;
    // Storage客户端(核心对象,用于与FastDFS交互)
    private final static StorageClient STORAGE_CLIENT;

    // 静态初始化:加载配置并创建StorageClient(仅执行一次)
    static {
        try {
            // 加载fdfs.properties配置文件
            PROPERTIES = new Properties();
            PROPERTIES.load(FastDFSUtils.class.getClassLoader().getResourceAsStream("fdfs.properties"));

            // 初始化FastDFS客户端配置
            ClientGlobal.initByProperties(PROPERTIES);

            // 创建Tracker客户端,获取Tracker Server连接
            TrackerClient trackerClient = new TrackerClient();
            TrackerServer trackerServer = trackerClient.getConnection();

            // 通过Tracker Server获取Storage Server(自动选择空闲节点)
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);

            // 创建Storage客户端(核心操作对象)
            STORAGE_CLIENT = new StorageClient(trackerServer, storageServer);
        } catch (Exception e) {
            // 初始化失败时,抛出Error终止虚拟机(工具类必须初始化成功)
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * 上传文件(通过输入流)
     * @param inputStream 文件输入流
     * @param fileName 文件原始名称(用于获取扩展名)
     * @return 上传结果:[0]分组名(如group1),[1]远程路径(如M00/00/00/xxx.jpg);null表示失败
     */
    public static String[] uploadFile(InputStream inputStream, String fileName) {
        try {
            // 1. 准备文件元数据(如原始文件名、大小)
            NameValuePair[] metaList = new NameValuePair[2];
            metaList[0] = new NameValuePair("file_name", fileName); // 原始文件名
            metaList[1] = new NameValuePair("file_length", inputStream.available() + ""); // 文件大小

            // 2. 将输入流转为字节数组(FastDFS要求字节数组传输)
            byte[] fileBuff = null;
            if (inputStream != null) {
                int len = inputStream.available();
                fileBuff = new byte[len];
                inputStream.read(fileBuff);
            }

            // 3. 调用FastDFS API上传文件
            // 参数:字节数组、文件扩展名(如jpg)、元数据
            return STORAGE_CLIENT.upload_file(fileBuff, getFileExt(fileName), metaList);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 下载文件
     * @param groupName 分组名(如group1)
     * @param remoteName 远程路径(如M00/00/00/xxx.jpg)
     * @return 文件输入流;null表示失败
     */
    public static InputStream downloadFile(String groupName, String remoteName) {
        try {
            // 调用FastDFS API下载文件(返回字节数组)
            byte[] fileBytes = STORAGE_CLIENT.download_file(groupName, remoteName);
            // 字节数组转为输入流(方便后续输出到客户端)
            return new ByteArrayInputStream(fileBytes);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 删除文件
     * @param groupName 分组名(如group1)
     * @param remoteName 远程路径(如M00/00/00/xxx.jpg)
     * @return 0表示成功,非0表示失败(错误码)
     */
    public static int deleteFile(String groupName, String remoteName) {
        try {
            // 调用FastDFS API删除文件
            return STORAGE_CLIENT.delete_file(groupName, remoteName);
        } catch (Exception e) {
            e.printStackTrace();
            return -1; // 自定义错误码,表示失败
        }
    }

    /**
     * 获取文件扩展名(不带点)
     * @param fileName 文件原始名称
     * @return 扩展名(如jpg);空字符串表示无扩展名
     */
    private static String getFileExt(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return "";
        }
        // 从最后一个点后面截取(如"test.jpg" → "jpg")
        return fileName.substring(fileName.lastIndexOf(".") + 1);
    }

    // 私有构造方法:禁止实例化工具类
    private FastDFSUtils() {}
}

6.5 业务层:Service 与 Mapper

6.5.1 实体类(MyFile.java)
package com.bjsxt.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;

/**
 * 文件实体类:对应tb_files表
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyFile implements Serializable {
    private Long id;
    private String fileName; // 文件原始名称
    private String groupName; // FastDFS分组名
    private String remoteName; // FastDFS远程路径
    private Long length; // 文件大小(字节)
    private Date createTime; // 上传时间

    /**
     * 生成文件预览URL(依赖Storage中的Nginx)
     * @return URL(如http://192.168.137.128:8888/group1/M00/00/00/xxx.jpg)
     */
    public String getUrl() {
        // Nginx默认监听8888端口,路径格式:http://IP:8888/分组名/远程路径
        return "http://192.168.137.128:8888/" + groupName + "/" + remoteName;
    }

    // 空setter:避免Thymeleaf报错(仅用于页面展示,无需设置)
    public void setUrl(String url) {}
}
6.5.2 Mapper 接口(MyFileMapper.java)
package com.bjsxt.mapper;

import com.bjsxt.pojo.MyFile;
import org.apache.ibatis.annotations.*;
import java.util.List;

/**
 * 文件Mapper:操作tb_files表
 */
@Mapper
public interface MyFileMapper {
    // 插入文件元数据
    @Insert("INSERT INTO tb_files(file_name, group_name, remote_name, length, create_time) " +
            "VALUES(#{fileName}, #{groupName}, #{remoteName}, #{length}, #{createTime})")
    int insert(MyFile myFile);

    // 查询所有文件
    @Select("SELECT id, file_name AS fileName, group_name AS groupName, " +
            "remote_name AS remoteName, length, create_time AS createTime FROM tb_files")
    List<MyFile> selectAll();

    // 根据分组与远程路径删除文件
    @Delete("DELETE FROM tb_files WHERE group_name = #{group} AND remote_name = #{remote}")
    int deleteByGroupAndRemote(@Param("group") String group, @Param("remote") String remote);
}
6.5.3 服务接口与实现(FileSystemService.java)
package com.bjsxt.service;

import com.bjsxt.pojo.MyFile;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;

/**
 * 文件服务接口
 */
public interface FileSystemService {
    boolean upload(MultipartFile file); // 上传文件
    List<MyFile> getFiles(); // 查询所有文件
    Map<String, Object> download(String group, String remote); // 下载文件
    boolean delete(String group, String remote); // 删除文件
}
package com.bjsxt.service.impl;

import com.bjsxt.mapper.MyFileMapper;
import com.bjsxt.pojo.MyFile;
import com.bjsxt.service.FileSystemService;
import com.bjsxt.util.FastDFSUtils;
import org.csource.common.NameValuePair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 文件服务实现类
 */
@Service
public class FileSystemServiceImpl implements FileSystemService {

    @Autowired
    private MyFileMapper myFileMapper;

    /**
     * 上传文件:1. 上传到FastDFS;2. 保存元数据到数据库
     */
    @Override
    @Transactional // 事务:确保两步操作同时成功或失败
    public boolean upload(MultipartFile file) {
        try {
            // 1. 调用FastDFS工具类上传文件
            String[] uploadResult = FastDFSUtils.uploadFile(file.getInputStream(), file.getOriginalFilename());
            if (uploadResult == null) {
                return false; // 上传FastDFS失败
            }

            // 2. 保存元数据到数据库
            MyFile myFile = new MyFile();
            myFile.setFileName(file.getOriginalFilename()); // 原始文件名
            myFile.setGroupName(uploadResult[0]); // 分组名
            myFile.setRemoteName(uploadResult[1]); // 远程路径
            myFile.setLength(file.getSize()); // 文件大小
            myFile.setCreateTime(new Date()); // 上传时间

            myFileMapper.insert(myFile);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 查询所有文件:从数据库获取元数据
     */
    @Override
    public List<MyFile> getFiles() {
        return myFileMapper.selectAll();
    }

    /**
     * 下载文件:1. 从FastDFS下载文件;2. 获取元数据(原始文件名)
     */
    @Override
    public Map<String, Object> download(String group, String remote) {
        try {
            // 1. 从FastDFS下载文件(输入流)
            byte[] fileBytes = FastDFSUtils.downloadFile(group, remote);
            if (fileBytes == null) {
                return null; // 下载失败
            }

            // 2. 返回结果:文件字节数组 + 原始文件名
            Map<String, Object> result = new HashMap<>();
            result.put("fileBytes", fileBytes);
            result.put("fileName", "test.jpg"); // 实际项目中可从元数据获取

            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 删除文件:1. 从FastDFS删除文件;2. 从数据库删除元数据
     */
    @Override
    @Transactional
    public boolean delete(String group, String remote) {
        try {
            // 1. 从FastDFS删除文件
            int deleteResult = FastDFSUtils.deleteFile(group, remote);
            if (deleteResult != 0) {
                return false; // FastDFS删除失败
            }

            // 2. 从数据库删除元数据
            myFileMapper.deleteByGroupAndRemote(group, remote);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

6.6 控制器:处理 HTTP 请求(FileSystemController.java)

package com.bjsxt.controller;

import com.bjsxt.pojo.MyFile;
import com.bjsxt.service.FileSystemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

/**
 * 文件控制器:处理上传、下载、页面跳转
 */
@Controller
public class FileSystemController {

    @Autowired
    private FileSystemService fileSystemService;

    /**
     * 跳转到首页:展示所有文件
     */
    @RequestMapping(value = {"/", "/index"})
    public String toIndex(Model model) {
        // 查询所有文件元数据,传递到页面
        List<MyFile> files = fileSystemService.getFiles();
        model.addAttribute("files", files);
        return "index"; // 跳转到templates/index.html
    }

    /**
     * 跳转到上传页面
     */
    @RequestMapping("/toUpload")
    public String toUpload() {
        return "upload"; // 跳转到templates/upload.html
    }

    /**
     * 处理文件上传
     */
    @RequestMapping("/upload")
    public String upload(MultipartFile file) {
        boolean success = fileSystemService.upload(file);
        if (success) {
            return "redirect:/"; // 上传成功,重定向到首页
        } else {
            return "redirect:/toUpload"; // 上传失败,重定向到上传页
        }
    }

    /**
     * 处理文件下载
     */
    @RequestMapping("/download")
    public void download(String group, String remote, HttpServletResponse response) {
        try {
            // 1. 调用服务层下载文件
            Map<String, Object> downloadResult = fileSystemService.download(group, remote);
            if (downloadResult == null) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
                return;
            }

            // 2. 设置响应头:告诉浏览器下载文件
            String fileName = URLEncoder.encode(downloadResult.get("fileName").toString(), "UTF-8");
            response.setContentType("application/octet-stream"); // 二进制流
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);

            // 3. 输出文件到客户端
            byte[] fileBytes = (byte[]) downloadResult.get("fileBytes");
            OutputStream os = response.getOutputStream();
            os.write(fileBytes);
            os.flush();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理文件删除
     */
    @RequestMapping("/delete")
    public String delete(String group, String remote) {
        fileSystemService.delete(group, remote);
        return "redirect:/"; // 删除后重定向到首页
    }
}

6.7 页面实现:Thymeleaf 模板

6.7.1 首页(index.html):展示文件列表与操作
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>FastDFS文件管理系统</title>
    <style>
        table { border-collapse: collapse; width: 800px; margin: 20px auto; }
        th, td { border: 1px solid #000; padding: 8px; text-align: center; }
    </style>
</head>
<body>
    <div style="text-align: center; margin-top: 20px;">
        <a href="/toUpload">上传文件</a>
    </div>

    <table>
        <tr>
            <th>编号</th>
            <th>原始文件名</th>
            <th>预览</th>
            <th>大小(字节)</th>
            <th>上传时间</th>
            <th>操作</th>
        </tr>
        <!-- 遍历文件列表 -->
        <tr th:each="file : ${files}">
            <td th:text="${file.id}"></td>
            <td th:text="${file.fileName}"></td>
            <!-- 预览图片:使用MyFile的getUrl()生成Nginx访问地址 -->
            <td><img th:src="${file.url}" style="height: 80px;" th:if="${file.fileName.endsWith('.jpg') or file.fileName.endsWith('.png')}"></td>
            <td th:text="${file.length}"></td>
            <td th:text="${#dates.format(file.createTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
            <!-- 下载与删除链接:传递分组与远程路径 -->
            <td>
                <a th:href="@{/download(group=${file.groupName}, remote=${file.remoteName})}">下载</a>
                &nbsp;&nbsp;
                <a th:href="@{/delete(group=${file.groupName}, remote=${file.remoteName})}">删除</a>
            </td>
        </tr>
    </table>
</body>
</html>
6.7.2 上传页(upload.html):文件上传表单
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
    <style>
        form { width: 600px; margin: 50px auto; }
        div { margin: 10px 0; }
    </style>
</head>
<body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <div>
            <label>选择文件:</label>
            <input type="file" name="file" required>
        </div>
        <div>
            <button type="submit">上传</button>
            <a href="/">回到首页</a>
        </div>
    </form>
</body>
</html>

6.8 启动类(TestFdfsClient.java)

package com.bjsxt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

七、文件在线预览:Nginx 的关键作用

FastDFS 本身不支持 HTTP 访问(仅通过 Client API 操作),但delron/fastdfs镜像中内置了 Nginx,通过ngx_fastdfs_module模块实现文件在线预览。

7.1 为什么需要 Nginx?

  • FastDFS 的 Storage Server 仅提供 API 接口,无法直接通过浏览器访问文件;
  • Nginx 是高性能 HTTP 服务器,可通过ngx_fastdfs_module将 HTTP 请求转发到 FastDFS,实现文件预览。

7.2 Nginx 核心配置解读

进入 Storage 容器,查看 Nginx 配置文件/usr/local/nginx/conf/nginx.conf

# 进入Storage容器
docker exec -it storage bash
# 查看配置文件
cat /usr/local/nginx/conf/nginx.conf

核心配置如下:

server {
    listen 8888;  # Nginx监听8888端口
    server_name localhost;

    # 匹配路径:groupX/M00(X为分组编号,如group1/M00)
    location ~/group([0-9])/M00 {
        ngx_fastdfs_module;  # 转发请求到FastDFS
    }
}
配置解读:
  • 监听端口:Nginx 默认监听 8888 端口,外部通过http://宿主机IP:8888访问;
  • 路径匹配~/group([0-9])/M00匹配如group1/M00/00/00/xxx.jpg的路径;
  • 模块转发ngx_fastdfs_module将请求转发到 Storage Server,获取文件并返回给浏览器。

7.3 预览原理

以图片预览为例,页面中img标签的srchttp://192.168.137.128:8888/group1/M00/00/00/xxx.jpg,请求流程如下:

  1. 浏览器发起 HTTP 请求到http://192.168.137.128:8888/group1/M00/00/00/xxx.jpg
  2. Nginx 监听 8888 端口,匹配路径group1/M00,触发ngx_fastdfs_module
  3. 模块将请求转发到 Storage Server,获取group1分组下M00/00/00/xxx.jpg的文件;
  4. Nginx 将文件内容返回给浏览器,实现预览。

八、总结与后续学习方向

8.1 FastDFS 核心优势回顾

  • 轻量易部署:Docker 镜像一键启动,无需复杂编译;
  • 高可用:Tracker 与 Storage 均无主从,分组备份避免数据丢失;
  • 高性能:256×256 目录结构、负载均衡,支持高并发访问;
  • 低成本:可使用廉价服务器搭建,支持在线扩容。

8.2 后续学习方向

  1. FastDFS 集群扩容:新增 Tracker/Storage 节点,扩展存储容量与性能;
  2. 高可用配置:部署多个 Tracker 节点,避免单点故障;
  3. 文件权限控制:结合 Spring Security,实现仅登录用户可预览 / 下载文件;
  4. 大文件上传优化:分片上传(如通过 WebUploader),避免大文件上传超时;
  5. 监控与告警:集成 Prometheus+Grafana,监控 FastDFS 磁盘使用、节点状态。

        通过本文,你已掌握 FastDFS 的核心原理、Docker 部署、代码实战与 Nginx 预览,可直接应用于实际项目中的文件存储需求。FastDFS 的学习重点在于 “理解架构设计” 与 “实战踩坑”,建议多动手尝试,遇到问题时查看官方社区或源码,加深理解。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐