FastDFS:从架构原理到实战落地,分布式文件存储全攻略
在分布式系统中,文件存储是绕不开的核心需求 —— 单体架构下 “本地存文件” 的简单方案,在多服务部署时会面临文件不一致、存储容量瓶颈、访问性能不足等问题。而FastDFS作为轻量级分布式文件系统,凭借其高性价比、易部署、支持负载均衡与容量扩展的特性,成为中小团队解决海量文件存储的首选方案。
本文基于系统学习笔记,从架构演进、原理剖析到实战落地,带你全面掌握 FastDFS,从 “知道是什么” 到 “能动手用”,再到 “理解为什么这么设计”。
目录
5.5.1 重启 Storage 容器报错:pid 文件冲突
6.3.1 FastDFS 配置(fdfs.properties)
6.5.2 Mapper 接口(MyFileMapper.java)
6.5.3 服务接口与实现(FileSystemService.java)
6.6 控制器:处理 HTTP 请求(FileSystemController.java)
6.7.1 首页(index.html):展示文件列表与操作
一、引言:为什么需要分布式文件存储?
在聊 FastDFS 之前,我们先搞清楚一个核心问题:为什么单体架构的文件管理方案会失效?
二、从单体到分布式:文件管理的架构演进
文件管理的方案必须适配整体系统架构,单体与分布式架构下的文件存储逻辑差异巨大,这也是我们引入 FastDFS 的根本原因。
2.1 单体架构的文件管理:简单但局限
单体架构中,应用服务(如 Tomcat)与文件资源存储在同一台服务器,客户端上传 / 下载文件直接操作本地磁盘:
- 优势:部署简单、本地读写速度快,无需额外维护文件服务;
- 痛点:
- 文件过多会占用服务器磁盘空间,影响应用服务性能;
- 绝对路径 / 相对路径依赖服务器环境,迁移困难;
- 一旦服务器故障,文件与应用同时不可用,无容灾能力。

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 官方资源
- 官方社区:ChinaUnix FastDFS 板块(作者余庆为版主,定期更新);
- 下载地址:SourceForge(最新版本 5.11)。
4.2 FastDFS 架构:面试必懂的核心设计
FastDFS 的架构极其简洁,仅包含三个核心角色,但却实现了负载均衡、容灾备份等复杂能力,是面试高频考点。
4.2.1 架构图与核心角色

FastDFS 的服务端仅包含两类节点,客户端通过 API 与服务端交互:
-
Client(客户端):
- 发起文件上传 / 下载 / 删除请求的应用(如 Java Spring Boot 项目);
- 通过 Tracker Server 获取 Storage Server 地址,再与 Storage Server 交互文件。
-
Tracker Server(跟踪服务器):
- 核心作用:调度与负载均衡,不存储文件数据;
- 维护内存中的集群状态:记录所有 Storage Server 的分组、IP、端口、磁盘使用情况;
- 无主从关系:多个 Tracker 节点平等,通过客户端随机选择实现高可用;
- 不与其他 Tracker 节点通信,仅接收 Storage 节点的状态上报。
-
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>
<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标签的src为http://192.168.137.128:8888/group1/M00/00/00/xxx.jpg,请求流程如下:
- 浏览器发起 HTTP 请求到
http://192.168.137.128:8888/group1/M00/00/00/xxx.jpg; - Nginx 监听 8888 端口,匹配路径
group1/M00,触发ngx_fastdfs_module; - 模块将请求转发到 Storage Server,获取
group1分组下M00/00/00/xxx.jpg的文件; - Nginx 将文件内容返回给浏览器,实现预览。
八、总结与后续学习方向
8.1 FastDFS 核心优势回顾
- 轻量易部署:Docker 镜像一键启动,无需复杂编译;
- 高可用:Tracker 与 Storage 均无主从,分组备份避免数据丢失;
- 高性能:256×256 目录结构、负载均衡,支持高并发访问;
- 低成本:可使用廉价服务器搭建,支持在线扩容。
8.2 后续学习方向
- FastDFS 集群扩容:新增 Tracker/Storage 节点,扩展存储容量与性能;
- 高可用配置:部署多个 Tracker 节点,避免单点故障;
- 文件权限控制:结合 Spring Security,实现仅登录用户可预览 / 下载文件;
- 大文件上传优化:分片上传(如通过 WebUploader),避免大文件上传超时;
- 监控与告警:集成 Prometheus+Grafana,监控 FastDFS 磁盘使用、节点状态。
通过本文,你已掌握 FastDFS 的核心原理、Docker 部署、代码实战与 Nginx 预览,可直接应用于实际项目中的文件存储需求。FastDFS 的学习重点在于 “理解架构设计” 与 “实战踩坑”,建议多动手尝试,遇到问题时查看官方社区或源码,加深理解。
更多推荐



所有评论(0)