FastDFS分布式文件系统
FastDFS 完整使用指南
本文档基于实际项目,全面讲解 FastDFS 分布式文件系统的使用方法、工作原理和最佳实践
📚 目录
- FastDFS 是什么
- 项目书写流程概览
- 详细开发步骤
- FastDFS 工作原理深度解析
- 完整的文件操作流程
- FastDFS 核心功能详解
- 常见问题与解答
- 下次开发时的快速上手指南
FastDFS 是什么
🎯 核心定位
FastDFS 是一个开源的分布式文件系统,专为互联网应用设计,具有:
- ✅ 高性能 - 单台服务器可支撑百万级文件存储
- ✅ 高可用 - 支持冗余备份,自动故障转移
- ✅ 分布式 - 支持横向扩展,无中心节点设计
- ✅ 负载均衡 - 自动负载均衡,智能选择存储服务器
- ✅ 轻量级 - 使用 C 语言实现,占用资源少
- ✅ 简单易用 - API 简单,易于集成
🤔 为什么需要 FastDFS?
不使用分布式文件系统的后果:
- ❌ 文件存储在应用服务器,占用大量磁盘空间
- ❌ 文件访问消耗应用服务器带宽和性能
- ❌ 水平扩展困难,无法共享文件
- ❌ 单点故障风险高
- ❌ 缺乏文件管理和监控手段
使用 FastDFS 的好处:
- ✅ 文件与应用分离,互不影响
- ✅ 支持高并发访问,提供 HTTP 服务
- ✅ 自动负载均衡和故障转移
- ✅ 支持主从备份,数据安全可靠
- ✅ 适合存储大量小文件(4KB - 500MB)
- ✅ 内置防盗链、限速、访问控制等功能
🏗️ FastDFS 架构组成
FastDFS 架构
│
├─ Tracker Server(跟踪服务器)
│ ├─ 管理 Storage Server
│ ├─ 记录文件存储位置
│ ├─ 负载均衡调度
│ └─ 集群协调
│
├─ Storage Server(存储服务器)
│ ├─ 实际存储文件
│ ├─ 提供文件上传下载
│ ├─ 文件同步备份
│ └─ 文件元数据管理
│
└─ Client(客户端)├─ 应用程序集成├─ 调用 FastDFS API└─ 连接 Tracker/Storage
角色说明:
-
Tracker Server(跟踪服务器)
- 负责调度和管理
- 记录所有 Storage Server 的状态
- 接收客户端请求,返回可用的 Storage Server
- 不存储实际文件,只存储元数据
- 支持集群部署(建议至少 2 台)
-
Storage Server(存储服务器)
- 实际存储文件
- 提供文件上传、下载、删除等操作
- 支持分组(Group),同组内服务器互为备份
- 每个组可以独立扩容
- 内置 HTTP 服务器(通过 Nginx 模块)
-
Client(客户端)
- 集成在应用程序中
- 通过 Java API 连接 FastDFS
- 上传时连接 Tracker 获取 Storage 地址
- 下载时可直接连接 Storage 或通过 HTTP
项目书写流程概览
📋 开发步骤清单
第一步:基础配置├─ pom.xml(Maven 依赖 - FastDFS 客户端)├─ application.yml(数据库配置、文件上传大小限制)├─ fdfs.properties(FastDFS 连接配置)└─ 数据库表结构设计(存储文件元信息)第二步:工具类开发 ⭐ 核心└─ FastDFSUtils.java(封装上传、下载、删除、修改操作)第三步:数据层开发├─ 实体类(Flower.java)└─ Mapper 接口(FlowerMapper.java)第四步:业务层开发├─ Service 接口(FlowerService.java)└─ Service 实现(FlowerServiceImpl.java - 调用 FastDFS 工具类)第五步:控制层开发└─ Controller(FlowerController.java - 处理文件上传下载)第六步:启动类└─ SpringBootMain.java第七步:前端页面├─ save.html(文件上传页面 - multipart/form-data)└─ success.html(文件展示页面 - 显示图片和下载链接)第八步:测试└─ DemoTest.java(单元测试 - 测试上传、下载、删除)
详细开发步骤
第一步:基础配置
1.1 创建 Maven 项目
为什么使用 Maven?
- 依赖管理自动化(不需要手动下载 jar 包)
- 统一的项目结构
- 方便的版本管理
- 简化项目打包和发布
项目结构:
fastdfs01/
├─ src/
│ ├─ main/
│ │ ├─ java/
│ │ │ └─ com/jr/
│ │ │ ├─ controller/ # 控制器
│ │ │ ├─ mapper/ # 数据访问层
│ │ │ ├─ pojo/ # 实体类
│ │ │ ├─ service/ # 业务层
│ │ │ ├─ util/ # 工具类(FastDFS 封装)
│ │ │ └─ SpringBootMain.java # 启动类
│ │ └─ resources/
│ │ ├─ application.yml # Spring Boot 配置
│ │ ├─ fdfs.properties # FastDFS 配置
│ │ ├─ static/ # 静态资源
│ │ └─ templates/ # Thymeleaf 模板
│ └─ test/ # 测试代码
└─ pom.xml # Maven 配置
1.2 配置 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"><modelVersion>4.0.0</modelVersion><groupId>com.jr.dz18</groupId><artifactId>fastdfs01</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 客户端核心依赖 --><dependency><groupId>cn.bestwu</groupId><artifactId>fastdfs-client-java</artifactId><version>1.27</version></dependency><!-- Apache Commons Lang3:提供字符串工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.4</version></dependency><!-- Spring Boot Web 启动器:提供 Web 功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Thymeleaf 模板引擎:用于渲染 HTML 页面 --><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><!-- JUnit 测试 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency></dependencies><build><!-- 资源拷贝插件:确保配置文件、页面等被正确打包 --><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource><resource><directory>src/main/resources</directory><includes><include>**/*.yml</include><include>**/*.xml</include><include>**/*.html</include><include>**/*.js</include><include>**/*.properties</include></includes></resource></resources></build>
</project>
💡 核心依赖说明:
fastdfs-client-java
:FastDFS 的 Java 客户端,提供文件操作 APIcommons-lang3
:字符串工具类,用于文件扩展名处理spring-boot-starter-web
:提供文件上传功能(MultipartFile)
1.3 配置 application.yml
# 服务器端口配置
server:port: 8080# Spring 配置
spring:# 文件上传配置servlet:multipart:max-file-size: 10MB # 单个文件最大大小max-request-size: 10MB # 请求最大大小(适用于多文件上传)# 数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/jdbc?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: root# MyBatis 配置
mybatis:# 实体类包路径type-aliases-package: com.jr.pojo# Mapper XML 文件位置mapper-locations: classpath:com/jr/mapper/*.xml# 配置configuration:# 控制台输出 SQL 语句(开发时方便调试)log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
⚙️ 配置说明:
max-file-size
:限制单个文件大小,防止超大文件占用资源max-request-size
:限制整个请求大小- 如果上传大文件,需要相应调整这两个参数
1.4 配置 fdfs.properties
# 连接超时时间(秒)
fastdfs.connect_timeout_in_seconds=10# 网络超时时间(秒)
fastdfs.network_timeout_in_seconds=30# 字符编码
fastdfs.charset=UTF-8# ⭐ Tracker 服务器地址(多个用逗号分隔)
# 格式:IP:端口号
# 端口号默认为 22122
fastdfs.tracker_servers=192.168.1.110:22122# HTTP 访问端口(如果配置了 Nginx)
# 默认为 8888,与 Storage 服务器的 Nginx 配置一致
# fastdfs.http_tracker_http_port=8888
🔧 配置说明:
-
tracker_servers:
- 这是最重要的配置
- 指定 Tracker Server 的地址和端口
- 如果有多个 Tracker,用逗号分隔
- 示例:
192.168.1.110:22122,192.168.1.111:22122
-
连接超时和网络超时:
- 根据网络环境调整
- 内网环境可以设置较小值(5-10 秒)
- 跨机房访问建议设置较大值(30-60 秒)
-
HTTP 端口:
- 用于直接通过 HTTP 访问文件
- 需要在 Storage Server 上配置 Nginx + FastDFS 模块
- 访问格式:
http://IP:8888/组名/文件路径
1.5 数据库表结构设计
核心表结构:
-- 花卉表(示例业务表)
CREATE TABLE `flower` (`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '花卉ID',`name` VARCHAR(50) NOT NULL COMMENT '花卉名称',`price` DOUBLE NOT NULL COMMENT '花卉价格',`production` VARCHAR(100) COMMENT '花卉产地',-- ⭐ FastDFS 相关字段`orname` VARCHAR(200) COMMENT '原始文件名',`groupname` VARCHAR(50) COMMENT 'FastDFS 组名(如:group1)',`remotefilename` VARCHAR(200) COMMENT 'FastDFS 远程文件路径'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花卉信息表';
💡 为什么要保存这些字段?
-
orname(原始文件名)
- 保存用户上传的原始文件名
- 用于下载时设置文件名
- 用于显示文件信息
-
groupname(组名)
- FastDFS 返回的组名(如:group1)
- 下载和删除时需要提供
- 示例:
group1
-
remotefilename(远程文件路径)
- FastDFS 返回的文件存储路径
- 下载和删除时需要提供
- 示例:
M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png
完整的文件访问 URL:
http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png\_____/ \___________________________________________/组名 远程文件路径
示例数据:
-- 插入测试数据
INSERT INTO flower VALUES
(1, '玫瑰花', 25.50, '云南', 'rose.jpg', 'group1', 'M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg');INSERT INTO flower VALUES
(2, '百合花', 30.00, '山东', 'lily.png', 'group1', 'M00/00/00/wKgBbmjd18yAHJK2BBq3vcdVOs9428.png');
第二步:FastDFS 工具类开发 ⭐ 核心
FastDFSUtils.java(完整版)
package com.jr.util;import org.apache.commons.lang3.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;import java.io.*;
import java.util.Properties;/*** FastDFS 工具类* * 功能:* 1. 文件上传(支持 InputStream 和 File)* 2. 文件下载* 3. 文件删除* 4. 文件修改* 5. 获取文件元数据* * 使用静态初始化块在类加载时初始化 FastDFS 客户端连接*/
public final class FastDFSUtils {/*** 定义静态属性,Properties 和 StorageClient* * Properties:存储 fdfs.properties 配置* StorageClient:FastDFS 存储客户端,用于文件操作*/private final static Properties PROPERTIES;private final static StorageClient STORAGE_CLIENT;/*** ⭐ 静态初始化代码块* * 作用:在类加载时执行,初始化 FastDFS 连接* * 执行时机:* 1. 第一次使用 FastDFSUtils 时* 2. 在任何方法调用之前* 3. 只执行一次(单例模式)* * 异常处理:* - 静态初始化块中的异常无法被外部捕获* - 抛出 ExceptionInInitializerError 终止类加载* - 确保不会在连接失败的情况下继续执行*/static {try {// 第一步:创建 Properties 对象PROPERTIES = new Properties();// 第二步:加载 fdfs.properties 配置文件// 使用类加载器从 classpath 读取配置文件PROPERTIES.load(FastDFSUtils.class.getClassLoader().getResourceAsStream("fdfs.properties"));// 第三步:使用 ClientGlobal 初始化 FastDFS 客户端全局配置// 解析配置文件中的 tracker_servers、超时时间等ClientGlobal.initByProperties(PROPERTIES);// 第四步:创建 Tracker 客户端对象TrackerClient trackerClient = new TrackerClient();// 第五步:连接到 Tracker Server// 返回 TrackerServer 对象,代表与 Tracker 的连接TrackerServer trackerServer = trackerClient.getConnection();// 第六步:通过 Tracker 获取可用的 Storage Server// Tracker 会根据负载均衡策略选择一个 StorageStorageServer storageServer = trackerClient.getStoreStorage(trackerServer);// 第七步:创建 Storage 客户端对象// 用于执行文件上传、下载、删除等操作STORAGE_CLIENT = new StorageClient(trackerServer, storageServer);} catch (Exception e) {// 静态初始化异常,抛出 Error 终止程序throw new ExceptionInInitializerError(e);}}/*** 文件上传(通过 InputStream)⭐ 推荐* * 优点:* 1. 支持保存文件元数据(原始文件名、文件大小)* 2. 适合处理 Web 上传(MultipartFile.getInputStream())* 3. 节省内存(流式处理)* * @param inputStream 上传的文件输入流* @param fileName 上传的文件原始名(用于提取扩展名和保存元数据)* @return String[2] - [0]:组名(如:group1),[1]:远程文件路径*/public static String[] uploadFile(InputStream inputStream, String fileName) {try {// 第一步:准备文件元数据(Meta Data)// 元数据会存储在 FastDFS 中,可以通过 API 查询NameValuePair[] meta_list = new NameValuePair[2];// 元数据1:原始文件名meta_list[0] = new NameValuePair("file name", fileName);// 元数据2:文件大小meta_list[1] = new NameValuePair("file length", inputStream.available() + "");// 第二步:将 InputStream 转换为字节数组byte[] file_buff = null;if (inputStream != null) {// 获取文件大小int len = inputStream.available();// 创建字节数组file_buff = new byte[len];// 读取输入流到字节数组inputStream.read(file_buff);}// 第三步:调用 FastDFS API 上传文件// 参数:// - file_buff:文件内容(字节数组)// - getFileExt(fileName):文件扩展名(如:jpg)// - meta_list:元数据String[] fileids = STORAGE_CLIENT.upload_file(file_buff, getFileExt(fileName), meta_list);// 第四步:返回结果// fileids[0] = 组名(如:group1)// fileids[1] = 远程文件路径(如:M00/00/00/xxx.jpg)return fileids;} catch (Exception ex) {ex.printStackTrace();return null;}}/*** 文件上传(通过 File 对象)* * 特点:不保存元数据* * 适用场景:* - 本地文件上传* - 批量处理文件* * @param file 文件对象* @param fileName 文件名* @return String[2] - [0]:组名,[1]:远程文件路径*/public static String[] uploadFile(File file, String fileName) {FileInputStream fis = null;try {// 不保存元数据NameValuePair[] meta_list = null;// 打开文件输入流fis = new FileInputStream(file);// 读取文件内容到字节数组byte[] file_buff = null;if (fis != null) {int len = fis.available();file_buff = new byte[len];fis.read(file_buff);}// 上传文件String[] fileids = STORAGE_CLIENT.upload_file(file_buff, getFileExt(fileName), meta_list);return fileids;} catch (Exception ex) {return null;} finally {// 关闭输入流if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}/*** 文件删除* * 注意:* - 删除操作不可逆!* - 删除后文件无法恢复* - 建议使用软删除(数据库标记删除,文件保留)* * @param groupName 组名(如:group1)* @param remoteFileName 远程文件路径(如:M00/00/00/xxx.jpg)* @return 0 为成功,非 0 为失败(具体错误代码)*/public static int deleteFile(String groupName, String remoteFileName) {try {// 调用 FastDFS API 删除文件// 如果 groupName 为空,默认使用 group1int result = STORAGE_CLIENT.delete_file(groupName == null ? "group1" : groupName, remoteFileName);return result;} catch (Exception ex) {return 0;}}/*** 文件修改* * 实现原理:* 1. 上传新文件* 2. 删除旧文件* * 注意:* - 不是真正的"修改",而是"替换"* - 文件路径会改变* - 需要更新数据库中的文件路径* * @param oldGroupName 旧文件组名* @param oldFileName 旧文件路径* @param file 新文件* @param fileName 新文件名* @return String[2] - [0]:新文件组名,[1]:新文件路径*/public static String[] modifyFile(String oldGroupName, String oldFileName, File file, String fileName) {String[] fileids = null;try {// 第一步:上传新文件fileids = uploadFile(file, fileName);if (fileids == null) {return null;}// 第二步:删除旧文件int delResult = deleteFile(oldGroupName, oldFileName);if (delResult != 0) {return null;}} catch (Exception ex) {return null;}return fileids;}/*** 文件下载* * 返回 InputStream,可以:* 1. 直接输出到浏览器(在线预览或下载)* 2. 保存到本地文件* 3. 进行其他处理(如:压缩、转码)* * @param groupName 组名* @param remoteFileName 远程文件路径* @return InputStream 文件输入流*/public static InputStream downloadFile(String groupName, String remoteFileName) {try {// 调用 FastDFS API 下载文件// 返回字节数组byte[] bytes = STORAGE_CLIENT.download_file(groupName, remoteFileName);// 将字节数组转换为 InputStreamInputStream inputStream = new ByteArrayInputStream(bytes);return inputStream;} catch (Exception ex) {return null;}}/*** 获取文件元数据* * 可以查询:* - 原始文件名* - 文件大小* - 上传时间* - 自定义元数据* * @param groupName 组名* @param remoteFileName 远程文件路径* @return NameValuePair[] 元数据数组*/public static NameValuePair[] getMetaDate(String groupName, String remoteFileName) {try {NameValuePair[] nvp = STORAGE_CLIENT.get_metadata(groupName, remoteFileName);return nvp;} catch (Exception ex) {ex.printStackTrace();return null;}}/*** 获取文件后缀名(不带点)* * 示例:* - "test.jpg" -> "jpg"* - "test.tar.gz" -> "gz"* - "test" -> ""* * @param fileName 文件名* @return 文件扩展名*/private static String getFileExt(String fileName) {if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {return "";} else {return fileName.substring(fileName.lastIndexOf(".") + 1);}}/*** 提供获取 Storage 客户端对象的工具方法* * 用于高级操作(如:Appender 文件、分片上传)* * @return StorageClient 对象*/public static StorageClient getStorageClient() {return STORAGE_CLIENT;}/*** 私有构造器,防止实例化* * 工具类不应该被实例化,所有方法都是静态的*/private FastDFSUtils() {}
}
🔑 核心理解点:
-
静态初始化块的作用:
- 在类加载时执行一次
- 初始化 FastDFS 连接
- 连接失败会抛出 Error 终止程序
-
为什么使用静态成员:
- 避免重复创建连接
- 提高性能(连接复用)
- 线程安全(StorageClient 是线程安全的)
-
上传方法的选择:
- Web 应用:使用
uploadFile(InputStream, String)
- 本地文件:使用
uploadFile(File, String)
- Web 应用:使用
-
文件路径的组成:
group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg \_____/ \___________________________________________/组名 远程文件路径
第三步:实体类
Flower.java
package com.jr.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;import java.io.Serializable;/*** 花卉实体类* 对应数据库的 flower 表*/
@Component // 注册为 Spring Bean
@AllArgsConstructor // Lombok:自动生成全参构造器
@NoArgsConstructor // Lombok:自动生成无参构造器
@Data // Lombok:自动生成 getter/setter/toString/equals/hashCode
public class Flower implements Serializable {private Integer id; // 花卉IDprivate String name; // 花卉名称private Double price; // 花卉价格private String production; // 花卉产地// ⭐ FastDFS 相关字段private String orname; // 原始文件名private String groupname; // FastDFS 组名(如:group1)private String remotefilename; // FastDFS 远程文件路径
}
💡 为什么要实现 Serializable?
- 支持对象序列化
- 可以存储到 Session
- 可以通过网络传输
- 可以缓存到 Redis
第四步:Mapper 层(数据访问层)
FlowerMapper.java
package com.jr.mapper;import com.jr.pojo.Flower;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;import java.util.List;/*** 花卉数据访问层*/
@Component
@Mapper // MyBatis 注解,标记为 Mapper 接口
public interface FlowerMapper {/*** 插入花卉信息* * 包含 FastDFS 返回的组名和文件路径* * @param flower 花卉对象* @return 影响行数*/@Insert("INSERT INTO flower VALUES(" +"DEFAULT, #{name}, #{price}, #{production}, " +"#{orname}, #{groupname}, #{remotefilename})")int insert(Flower flower);/*** 查询所有花卉* * 用于前端展示列表* * @return 花卉列表*/@Select("SELECT * FROM flower")List<Flower> selectAll();
}
🔍 SQL 解析:
-- 插入语句
INSERT INTO flower VALUES(DEFAULT, -- id 自增'玫瑰花', -- name25.50, -- price'云南', -- production'rose.jpg', -- orname(原始文件名)'group1', -- groupname(FastDFS 组名)'M00/00/00/xxx.jpg' -- remotefilename(FastDFS 路径)
)
第五步:Service 层(业务层)
5.1 FlowerService.java(接口)
package com.jr.service;import com.jr.pojo.Flower;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.util.List;/*** 花卉业务接口*/
public interface FlowerService {/*** 保存花卉信息(包含文件上传)* * @param flower 花卉信息* @param photo 上传的文件* @return 影响行数* @throws IOException IO 异常*/int save(Flower flower, MultipartFile photo) throws IOException;/*** 查询所有花卉* * @return 花卉列表*/List<Flower> findAll();
}
5.2 FlowerServiceImpl.java(实现类)⭐ 核心
package com.jr.service.Impl;import com.jr.mapper.FlowerMapper;
import com.jr.pojo.Flower;
import com.jr.service.FlowerService;
import com.jr.util.FastDFSUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;
import java.util.List;/*** 花卉业务实现类* * ⭐ 核心逻辑:* 1. 将上传的文件保存到 FastDFS* 2. 获取 FastDFS 返回的组名和文件路径* 3. 将文件信息和业务数据一起保存到数据库*/
@Service // 注册为 Spring Bean
public class FlowerServiceImpl implements FlowerService {@Autowiredprivate FlowerMapper flowerMapper;/*** 保存花卉信息(包含文件上传)* * 执行流程:* 1. 获取上传文件的输入流* 2. 调用 FastDFSUtils 上传文件到 FastDFS* 3. 获取 FastDFS 返回的组名和路径* 4. 将文件信息设置到 flower 对象* 5. 插入数据库* * @param flower 花卉信息* @param photo 上传的文件(MultipartFile)* @return 影响行数* @throws IOException IO 异常*/@Overridepublic int save(Flower flower, MultipartFile photo) throws IOException {// 第一步:获取上传文件的输入流InputStream inputStream = photo.getInputStream();// 第二步:调用 FastDFSUtils 上传文件// 返回数组:[0]=组名,[1]=文件路径String[] strings = FastDFSUtils.uploadFile(inputStream, photo.getOriginalFilename());// 第三步:设置文件信息到 flower 对象flower.setOrname(photo.getOriginalFilename()); // 原始文件名flower.setGroupname(strings[0]); // 组名(如:group1)flower.setRemotefilename(strings[1]); // 文件路径(如:M00/00/00/xxx.jpg)// 第四步:插入数据库return flowerMapper.insert(flower);}/*** 查询所有花卉* * @return 花卉列表*/@Overridepublic List<Flower> findAll() {return flowerMapper.selectAll();}
}
🔍 深度解析:MultipartFile 是什么?
// MultipartFile 是 Spring 提供的文件上传接口
// 常用方法:MultipartFile photo = ...;// 1. 获取原始文件名
String fileName = photo.getOriginalFilename();
// 示例:rose.jpg// 2. 获取文件大小(字节)
long size = photo.getSize();
// 示例:1024000(约 1MB)// 3. 获取文件类型(MIME Type)
String contentType = photo.getContentType();
// 示例:image/jpeg// 4. 获取输入流(⭐ 最常用)
InputStream inputStream = photo.getInputStream();
// 用于读取文件内容// 5. 判断是否为空
boolean isEmpty = photo.isEmpty();
// true 表示用户没有选择文件// 6. 保存到本地(不推荐,应该用 FastDFS)
photo.transferTo(new File("D:/upload/rose.jpg"));
第六步:Controller 层
FlowerController.java
package com.jr.controller;import com.jr.pojo.Flower;
import com.jr.service.FlowerService;
import com.jr.util.FastDFSUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;/*** 花卉控制器* * 功能:* 1. 页面路由* 2. 文件上传* 3. 文件下载* 4. 数据查询*/
@Controller
public class FlowerController {@Autowiredprivate FlowerService flowerService;/*** 动态路由处理* * 示例:* - 访问 /save → 返回 "save" → 渲染 save.html* - 访问 /success → 返回 "success" → 渲染 success.html* * @param url 路径变量* @return 视图名称*/@RequestMapping("/{url}")public String url(@PathVariable String url) {return url;}/*** 文件上传处理 ⭐ 核心* * 处理流程:* 1. 接收表单参数(flower 对象)* 2. 接收上传的文件(photo)* 3. 调用 Service 保存(包含文件上传到 FastDFS)* 4. 返回结果页面* * @param flower 花卉信息* @param photo 上传的文件* @return 视图名称* @throws IOException IO 异常*/@RequestMapping("/save1")public String save(Flower flower, MultipartFile photo) throws IOException {int save = flowerService.save(flower, photo);if (save > 0) {return "success"; // 跳转到成功页面} else {return "save"; // 返回上传页面}}/*** 查询所有花卉(Ajax 接口)* * @ResponseBody:将返回值转换为 JSON* * 返回示例:* [* {* "id": 1,* "name": "玫瑰花",* "price": 25.5,* "production": "云南",* "orname": "rose.jpg",* "groupname": "group1",* "remotefilename": "M00/00/00/xxx.jpg"* }* ]* * @return 花卉列表(JSON)*/@RequestMapping("/getAll")@ResponseBodypublic List<Flower> getAll() {return flowerService.findAll();}/*** 文件下载 ⭐ 核心* * 处理流程:* 1. 从 FastDFS 下载文件(得到 InputStream)* 2. 设置响应头(告诉浏览器这是一个下载文件)* 3. 将 InputStream 写入响应的 OutputStream* 4. 浏览器弹出下载对话框* * @param gname 组名(如:group1)* @param orname 远程文件路径(如:M00/00/00/xxx.jpg)* @param response HttpServletResponse 对象* @throws IOException IO 异常*/@RequestMapping("/download")@ResponseBodypublic void download(String gname, String orname, HttpServletResponse response) throws IOException {// 第一步:生成随机文件名(防止中文乱码)// UUID 确保文件名唯一String uuname = UUID.randomUUID() + ".png";// 第二步:设置响应头// content-disposition:告诉浏览器这是一个附件,需要下载// attachment:以附件形式下载// filename:下载时的文件名response.setHeader("content-disposition", "attachment;filename=" + uuname);// 第三步:从 FastDFS 下载文件InputStream inputStream = FastDFSUtils.downloadFile(gname, orname);// 第四步:获取响应的输出流ServletOutputStream outputStream = response.getOutputStream();// 第五步:将输入流的内容复制到输出流// IOUtils.copy():Apache Commons IO 提供的工具方法IOUtils.copy(inputStream, outputStream);// 第六步:关闭流outputStream.close();inputStream.close();}
}
🔑 关键理解点:
-
MultipartFile 参数自动绑定:
// 表单中的 name="photo" 会自动绑定到参数 public String save(Flower flower, MultipartFile photo)
-
文件下载的响应头:
// attachment:附件(下载) response.setHeader("content-disposition", "attachment;filename=" + fileName);// inline:内联(在线预览,适用于图片、PDF) response.setHeader("content-disposition", "inline;filename=" + fileName);
-
流的复制:
// 手动复制(不推荐) byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, len); }// 使用工具类(推荐) IOUtils.copy(inputStream, outputStream);
第七步:启动类
SpringBootMain.java
package com.jr;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** Spring Boot 启动类*/
@SpringBootApplication // 标记为 Spring Boot 应用
public class SpringBootMain {public static void main(String[] args) {SpringApplication.run(SpringBootMain.class, args);System.out.println("========================================");System.out.println("⭐ FastDFS 应用启动成功!");System.out.println("⭐ 访问地址:http://localhost:8080/save");System.out.println("========================================");}
}
启动时会发生什么?
1. Spring Boot 启动
2. 加载 FastDFSUtils 类
3. 执行静态初始化块- 读取 fdfs.properties- 连接 Tracker Server- 创建 StorageClient
4. 扫描所有 @Component, @Service, @Controller 注解的类
5. 创建 Bean 并注入依赖关系
6. MyBatis 扫描 Mapper 接口
7. Thymeleaf 配置模板路径
8. 启动内置 Tomcat,监听 8080 端口
9. 应用就绪,可以接受请求
第八步:前端页面
8.1 save.html(文件上传页面)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>花卉添加</title><style>body {font-family: Arial, sans-serif;background-color: #f5f5f5;padding: 50px;}h2 {color: #333;}form {background-color: white;padding: 30px;border-radius: 10px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);max-width: 500px;}p {margin-bottom: 15px;}input[type="text"], input[type="file"] {width: 100%;padding: 10px;border: 1px solid #ddd;border-radius: 5px;box-sizing: border-box;}input[type="submit"] {width: 100%;padding: 12px;background-color: #4CAF50;color: white;border: none;border-radius: 5px;cursor: pointer;font-size: 16px;}input[type="submit"]:hover {background-color: #45a049;}</style>
</head>
<body><h2>花卉信息添加</h2><!-- ⭐ 文件上传表单的三个要点:1. method="post" - 必须是 POST 请求2. enctype="multipart/form-data" - 必须设置(支持文件上传)3. input type="file" - 文件选择控件--><form action="/save1" method="post" enctype="multipart/form-data"><p>花卉名称:<input type="text" name="name" required/></p><p>花卉价格:<input type="text" name="price" required/></p><p>花卉产地:<input type="text" name="production" required/></p><p>花卉图片:<input type="file" name="photo" accept="image/*" required/></p><p><input type="submit" value="提交"/></p></form><p style="margin-top: 20px;"><a href="/success">查看已上传的花卉</a></p>
</body>
</html>
🔑 关键点:
-
enctype=“multipart/form-data”
- 必须设置!否则无法上传文件
- 告诉浏览器使用 multipart 编码
- 支持文件二进制传输
-
name 属性的对应关系
<!-- 前端 --> <input type="text" name="name"/> <input type="file" name="photo"/><!-- 后端 --> public String save(Flower flower, MultipartFile photo) // name 字段自动绑定到 flower.name // photo 字段自动绑定到 photo 参数
-
accept 属性
<!-- 只允许上传图片 --> <input type="file" accept="image/*"/><!-- 只允许上传 PDF --> <input type="file" accept="application/pdf"/><!-- 允许多种类型 --> <input type="file" accept="image/*,.pdf,.doc,.docx"/>
8.2 success.html(文件展示页面)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>花卉列表</title><!-- 引入 jQuery --><script type="text/javascript" src="../js/jquery-1.8.3.js"></script><style>body {font-family: Arial, sans-serif;background-color: #f5f5f5;padding: 50px;}h2 {color: #333;}table {width: 100%;background-color: white;border-collapse: collapse;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}thead {background-color: #4CAF50;color: white;}td {padding: 12px;text-align: center;border-bottom: 1px solid #ddd;}img {cursor: pointer;transition: transform 0.3s;}img:hover {transform: scale(3);}a {color: #4CAF50;text-decoration: none;}a:hover {text-decoration: underline;}</style><script type="text/javascript">$(document).ready(function () {// 页面加载完成后,发送 Ajax 请求获取花卉列表$.get("getAll", function (dt) {// dt 是服务器返回的 JSON 数组JSON.stringify(dt);// 清空表格内容$("tbody").empty();// 遍历数据,动态生成表格行for (var i = 0; i < dt.length; i++) {// 构造完整的图片 URL// 格式:http://IP:端口/组名/文件路径var imgUrl = 'http://192.168.1.110:8888/' + dt[i].groupname + '/' + dt[i].remotefilename;// 创建表格行$("<tr>" +"<td>" + dt[i].id + "</td>" +"<td>" + dt[i].name + "</td>" +"<td>" + dt[i].price + "</td>" +"<td>" + dt[i].production + "</td>" +"<td>" +"<img height='20px' width='20px' " +"title='" + dt[i].orname + "' " +"src='" + imgUrl + "'/>" +"</td>" +"<td>" +"<a href='download?gname=" + dt[i].groupname + "&&orname=" + dt[i].remotefilename + "'>下载</a>" +"</td>" +"</tr>").appendTo("tbody");}});});</script>
</head>
<body><h2>花卉信息列表</h2><table><thead><tr><td>花卉编号</td><td>花卉名称</td><td>价钱</td><td>产地</td><td>图片</td><td>操作</td></tr></thead><tbody></tbody></table><p style="margin-top: 20px;"><a href="/save">添加新花卉</a></p>
</body>
</html>
🔍 图片访问原理:
1. 前端构造 URL:http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg2. 浏览器发送请求到 Storage Server 的 Nginx3. Nginx + FastDFS 模块解析请求:- 组名:group1- 文件路径:M00/00/00/xxx.jpg4. Nginx 从磁盘读取文件:/data/fastdfs/storage/data/M00/00/00/xxx.jpg5. 返回文件内容给浏览器6. 浏览器显示图片
第九步:单元测试
DemoTest.java
package com.jr;import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.junit.Test;
import com.jr.util.FastDFSUtils;import java.io.*;
import java.util.Arrays;/*** FastDFS 功能测试*/
public class DemoTest {/*** 测试文件上传* * 两种上传方式:* 1. File 对象:不保存元数据* 2. InputStream:保存元数据(推荐)*/@Testpublic void test01() throws FileNotFoundException {// 方式1:使用 File 上传(不保存元数据)/*String[] strings = FastDFSUtils.uploadFile(new File("C:\\Users\\CuiDa\\Desktop\\壁纸\\1.png"), "1.png");System.out.println(Arrays.toString(strings));*/// 方式2:使用 InputStream 上传(保存元数据)⭐ 推荐String[] strings = FastDFSUtils.uploadFile(new FileInputStream(new File("C:\\Users\\CuiDa\\Desktop\\壁纸\\1.png")),"1.png");// 输出结果:[group1, M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png]System.out.println(Arrays.toString(strings));System.out.println("组名:" + strings[0]);System.out.println("文件路径:" + strings[1]);// ⭐ 访问 URL:// http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png}/*** 测试文件下载*/@Testpublic void test2() throws IOException {// 第一步:从 FastDFS 下载文件(得到 InputStream)InputStream inputStream = FastDFSUtils.downloadFile("group1", "M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png");// 第二步:指定本地保存路径OutputStream outputStream = new FileOutputStream("D:\\fastdfs\\11.png");// 第三步:复制流IOUtils.copy(inputStream, outputStream);// 第四步:关闭流inputStream.close();outputStream.close();System.out.println("文件下载成功!保存到:D:\\fastdfs\\11.png");}/*** 测试文件删除*/@Testpublic void test3() {// 删除文件// 返回值:0 表示成功,非 0 表示失败int result = FastDFSUtils.deleteFile("group1", "M00/00/00/wKgBbmjeGuyAWIWOAAp2ubcUNr8520.png");if (result == 0) {System.out.println("文件删除成功!");} else {System.out.println("文件删除失败!错误代码:" + result);}}
}
FastDFS 工作原理深度解析
🔍 核心概念
1. FastDFS 文件上传流程
客户端(应用程序)↓
① 发送上传请求到 Tracker Server"我要上传一个文件,请分配 Storage"↓
Tracker Server↓
② 根据负载均衡策略选择一个 Storage Server策略:轮询、按剩余空间、按上传次数等↓
③ 返回 Storage Server 的 IP 和端口"你可以将文件上传到 192.168.1.110:23000"↓
客户端↓
④ 连接到指定的 Storage Server↓
⑤ 上传文件内容(二进制流)↓
Storage Server↓
⑥ 将文件保存到磁盘路径生成规则:/data/fastdfs/storage/data/M00/00/00/xxx.jpg⑦ 生成文件ID(包含组名和路径)group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg⑧ 如果配置了主从复制,同步到从Storage↓
⑨ 返回文件ID给客户端↓
客户端↓
⑩ 保存文件ID到数据库
💡 关键理解点:
-
Tracker 不存储文件
- Tracker 只负责调度和管理
- 文件实际存储在 Storage Server
- Tracker 存储的是元数据(哪个文件在哪个 Storage)
-
文件路径的含义
M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg \_/ \___/ \________________________________/| | || | 文件名(自动生成,包含时间戳、IP等信息)| 二级目录(根据文件数量自动创建)存储路径(M00 对应配置文件中的第一个 store_path)
-
负载均衡策略
- 轮询(Round Robin)
- 随机(Random)
- 按剩余空间(Free Space)
- 按上传次数(Upload Count)
2. FastDFS 文件下载流程
方式1:通过客户端API下载
客户端↓
① 发送下载请求到 Tracker(提供文件ID)"我要下载 group1/M00/00/00/xxx.jpg"↓
Tracker Server↓
② 根据组名(group1)查找对应的 Storage Server↓
③ 返回 Storage Server 的 IP 和端口"文件在 192.168.1.110:23000"↓
客户端↓
④ 连接到 Storage Server↓
⑤ 发送文件路径↓
Storage Server↓
⑥ 从磁盘读取文件↓
⑦ 返回文件内容(二进制流)↓
客户端↓
⑧ 接收文件内容
方式2:通过HTTP直接访问(推荐)
浏览器↓
① 访问 URLhttp://192.168.1.110:8888/group1/M00/00/00/xxx.jpg↓
Nginx(Storage Server 上)↓
② FastDFS Nginx 模块解析 URL- 组名:group1- 文件路径:M00/00/00/xxx.jpg↓
③ 根据路径读取文件/data/fastdfs/storage/data/M00/00/00/xxx.jpg↓
④ 返回文件内容↓
浏览器↓
⑤ 显示图片或下载文件
💡 为什么推荐HTTP方式?
- ✅ 直接访问,无需经过应用服务器
- ✅ 减轻应用服务器压力
- ✅ 充分利用 Nginx 的性能优势
- ✅ 支持浏览器缓存
- ✅ 支持断点续传
3. FastDFS 文件同步机制
主 Storage Server 从 Storage Server↓ ↑
① 接收客户端上传 |↓ |
② 保存文件到磁盘 |↓ |
③ 将文件写入 binlog |↓ |
④ 通过 binlog 同步到从 Storage ----------┘
同步特点:
- 异步同步(不影响上传性能)
- 断点续传(网络故障后自动恢复)
- 增量同步(只同步变化的文件)
- 一主多从(一个主 Storage 可以有多个从 Storage)
完整的文件操作流程
场景 1:用户上传图片
用户在浏览器选择图片(rose.jpg)→ 点击"提交"按钮↓
POST /save1(multipart/form-data)↓
1. FlowerController.save() 接收请求- Flower 对象:{name:"玫瑰花", price:25.5, production:"云南"}- MultipartFile 对象:{originalFilename:"rose.jpg", size:102400, ...}↓
2. FlowerService.save() 业务处理- 获取文件输入流:photo.getInputStream()↓
3. FastDFSUtils.uploadFile() 上传到 FastDFS- 连接 Tracker Server- Tracker 返回 Storage Server 地址- 连接 Storage Server- 上传文件内容- Storage 保存文件到磁盘- 返回文件ID:["group1", "M00/00/00/xxx.jpg"]↓
4. 设置文件信息到 Flower 对象- flower.setOrname("rose.jpg")- flower.setGroupname("group1")- flower.setRemotefilename("M00/00/00/xxx.jpg")↓
5. FlowerMapper.insert() 保存到数据库- 插入记录到 flower 表↓
6. 返回 "success" 视图↓
7. Thymeleaf 渲染 success.html↓
8. 浏览器显示成功页面
场景 2:用户查看图片列表
用户访问:http://localhost:8080/success↓
1. FlowerController.url("success")- 返回 "success" 视图↓
2. Thymeleaf 渲染 success.html- 返回 HTML 页面给浏览器↓
3. 浏览器执行 JavaScript- jQuery 发送 Ajax 请求:$.get("/getAll")↓
4. FlowerController.getAll()- 调用 FlowerService.findAll()- 调用 FlowerMapper.selectAll()- 从数据库查询所有花卉记录- 返回 JSON 数组↓
5. JavaScript 接收数据- 遍历数据,动态生成表格行- 构造图片 URL:http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg- 插入到表格↓
6. 浏览器加载图片- 向 FastDFS 的 Nginx 发送请求- Nginx 返回图片内容- 浏览器显示图片
场景 3:用户下载文件
用户点击"下载"链接↓
GET /download?gname=group1&orname=M00/00/00/xxx.jpg↓
1. FlowerController.download()- 接收参数:gname="group1", orname="M00/00/00/xxx.jpg"↓
2. FastDFSUtils.downloadFile()- 连接 Tracker Server- Tracker 返回 Storage Server 地址- 连接 Storage Server- 发送下载请求- Storage 返回文件内容(字节数组)- 转换为 InputStream↓
3. 设置响应头- content-disposition: attachment;filename=xxx.png- 告诉浏览器这是一个下载文件↓
4. 将 InputStream 写入响应流- IOUtils.copy(inputStream, outputStream)↓
5. 浏览器弹出下载对话框- 用户选择保存位置- 文件保存到本地
FastDFS 核心功能详解
功能 1:文件上传
实现方式:
// 方式1:通过 InputStream(推荐)
InputStream is = multipartFile.getInputStream();
String[] result = FastDFSUtils.uploadFile(is, "photo.jpg");// 方式2:通过 File 对象
File file = new File("D:/test.jpg");
String[] result = FastDFSUtils.uploadFile(file, "test.jpg");
返回值:
String[] result = ["group1", "M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg"];
result[0] // 组名
result[1] // 文件路径
完整的访问URL:
http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg
\___________________/\_____/\___________________________________________/服务器地址 组名 文件路径
功能 2:文件下载
实现方式:
// 下载文件(返回 InputStream)
InputStream is = FastDFSUtils.downloadFile("group1", "M00/00/00/xxx.jpg");// 保存到本地
FileOutputStream fos = new FileOutputStream("D:/download.jpg");
IOUtils.copy(is, fos);
fos.close();
is.close();// 或者直接输出到浏览器(在 Controller 中)
ServletOutputStream os = response.getOutputStream();
IOUtils.copy(is, os);
os.close();
is.close();
功能 3:文件删除
实现方式:
int result = FastDFSUtils.deleteFile("group1", "M00/00/00/xxx.jpg");
if (result == 0) {System.out.println("删除成功");
} else {System.out.println("删除失败,错误代码:" + result);
}
注意事项:
- ⚠️ 删除操作不可逆!
- ⚠️ 建议使用软删除(数据库标记,文件保留)
- ⚠️ 删除前确认文件没有被其他地方引用
功能 4:文件修改
实现方式:
// 上传新文件并删除旧文件
String[] result = FastDFSUtils.modifyFile("group1", // 旧文件组名"M00/00/00/old.jpg", // 旧文件路径new File("D:/new.jpg"), // 新文件"new.jpg" // 新文件名
);// 更新数据库中的文件路径
flower.setGroupname(result[0]);
flower.setRemotefilename(result[1]);
flowerMapper.update(flower);
功能 5:获取文件元数据
实现方式:
NameValuePair[] metadata = FastDFSUtils.getMetaDate("group1", "M00/00/00/xxx.jpg");
for (NameValuePair pair : metadata) {System.out.println(pair.getName() + " = " + pair.getValue());
}// 输出:
// file name = rose.jpg
// file length = 102400
常见问题与解答
Q1:FastDFS 和传统文件存储的区别?
A: 主要区别:
特性 | 传统存储(应用服务器) | FastDFS(分布式) |
---|---|---|
存储位置 | 应用服务器磁盘 | 独立的存储服务器 |
扩展性 | 难以扩展 | 易于横向扩展 |
性能 | 占用应用服务器资源 | 专用存储,性能高 |
高可用 | 单点故障 | 支持主从备份 |
负载均衡 | 需要手动实现 | 自动负载均衡 |
访问方式 | 通过应用服务器 | 直接 HTTP 访问 |
Q2:上传文件时出现"连接超时"错误怎么办?
A: 排查步骤:
-
检查 Tracker Server 是否启动
# Linux 命令 ps -ef | grep fdfs_trackerd# 如果没有运行,启动 Tracker /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
-
检查网络连接
# 测试端口是否开放 telnet 192.168.1.110 22122# 检查防火墙 firewall-cmd --list-ports
-
检查配置文件
# fdfs.properties fastdfs.tracker_servers=192.168.1.110:22122 # IP 和端口是否正确? fastdfs.connect_timeout_in_seconds=10 # 超时时间是否太短?
-
增加超时时间
fastdfs.connect_timeout_in_seconds=30 fastdfs.network_timeout_in_seconds=60
Q3:图片无法显示(404 错误)怎么办?
A: 排查步骤:
-
检查 Storage Server 的 Nginx 是否启动
ps -ef | grep nginx# 启动 Nginx /usr/local/nginx/sbin/nginx
-
检查 Nginx 配置
# /usr/local/nginx/conf/nginx.conf location ~ /group[0-9]/ {ngx_fastdfs_module; }
-
检查访问 URL 是否正确
正确格式: http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg常见错误: - 缺少组名:http://...//M00/00/00/xxx.jpg - 端口错误:http://...:22122/... (应该是 8888) - 路径错误:http://.../group1/M00/xxx.jpg (缺少目录)
-
检查文件是否真实存在
# 在 Storage Server 上查看 ls -l /data/fastdfs/storage/data/M00/00/00/
Q4:文件上传大小限制怎么调整?
A: 需要修改多处配置:
-
Spring Boot 配置(application.yml)
spring:servlet:multipart:max-file-size: 100MB # 单个文件最大 100MBmax-request-size: 100MB # 请求最大 100MB
-
Nginx 配置(如果通过 Nginx 上传)
# /usr/local/nginx/conf/nginx.conf http {client_max_body_size 100m; # 允许上传 100MB }
-
重启服务
# 重启 Nginx /usr/local/nginx/sbin/nginx -s reload# 重启 Spring Boot 应用
Q5:如何实现文件秒传(相同文件只存一份)?
A: FastDFS 默认不支持,需要自己实现:
@Service
public class FileService {/*** 文件上传(支持秒传)*/public String upload(MultipartFile file) throws Exception {// 1. 计算文件 MD5String md5 = DigestUtils.md5Hex(file.getInputStream());// 2. 查询数据库,看是否已存在相同 MD5 的文件FileInfo existFile = fileMapper.selectByMd5(md5);if (existFile != null) {// 文件已存在,秒传成功(返回已有的文件路径)return existFile.getFilePath();}// 3. 文件不存在,上传到 FastDFSString[] result = FastDFSUtils.uploadFile(file.getInputStream(), file.getOriginalFilename());// 4. 保存文件信息到数据库(包含 MD5)FileInfo fileInfo = new FileInfo();fileInfo.setMd5(md5);fileInfo.setGroupName(result[0]);fileInfo.setRemoteFileName(result[1]);fileMapper.insert(fileInfo);return fileInfo.getFilePath();}
}
下次开发时的快速上手指南
🚀 快速开发步骤(FastDFS 项目)
第一步:引入依赖
<!-- pom.xml -->
<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>
第二步:配置文件
# fdfs.properties
fastdfs.connect_timeout_in_seconds=10
fastdfs.network_timeout_in_seconds=30
fastdfs.charset=UTF-8
fastdfs.tracker_servers=192.168.1.110:22122
# application.yml
spring:servlet:multipart:max-file-size: 10MBmax-request-size: 10MB
第三步:复制工具类
复制 FastDFSUtils.java 到项目的 util 包
第四步:实体类添加字段
private String orname; // 原始文件名
private String groupname; // FastDFS 组名
private String remotefilename; // FastDFS 文件路径
第五步:Service 层调用
@Service
public class FileServiceImpl {public int upload(Entity entity, MultipartFile file) throws IOException {// 上传到 FastDFSString[] result = FastDFSUtils.uploadFile(file.getInputStream(), file.getOriginalFilename());// 设置文件信息entity.setOrname(file.getOriginalFilename());entity.setGroupname(result[0]);entity.setRemotefilename(result[1]);// 保存到数据库return mapper.insert(entity);}
}
第六步:Controller 处理
@Controller
public class FileController {// 上传@RequestMapping("/upload")public String upload(Entity entity, MultipartFile file) throws IOException {service.upload(entity, file);return "success";}// 下载@RequestMapping("/download")@ResponseBodypublic void download(String gname, String rname, HttpServletResponse response) throws IOException {response.setHeader("content-disposition", "attachment;filename=" + UUID.randomUUID() + ".jpg");InputStream is = FastDFSUtils.downloadFile(gname, rname);IOUtils.copy(is, response.getOutputStream());is.close();}
}
第七步:前端页面
<!-- 上传表单 -->
<form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="file" required/><button type="submit">上传</button>
</form><!-- 显示图片 -->
<img src="http://192.168.1.110:8888/{{groupname}}/{{remotefilename}}"/><!-- 下载链接 -->
<a href="/download?gname={{groupname}}&rname={{remotefilename}}">下载</a>
📋 核心配置清单
配置项 | 说明 | 示例 |
---|---|---|
tracker_servers | Tracker 服务器地址 | 192.168.1.110:22122 |
connect_timeout | 连接超时时间(秒) | 10 |
network_timeout | 网络超时时间(秒) | 30 |
max-file-size | 最大文件大小 | 10MB |
HTTP 端口 | Nginx 端口 | 8888 |
🎯 最佳实践
-
文件命名规范
使用 FastDFS 自动生成的文件名,不要自定义 原因: - 自动包含时间戳 - 自动包含服务器信息 - 防止文件名冲突
-
数据库设计
-- 必须保存的字段 CREATE TABLE file_info (orname VARCHAR(200), -- 原始文件名groupname VARCHAR(50), -- 组名remotefilename VARCHAR(200) -- 文件路径 );-- 可选字段 file_size BIGINT, -- 文件大小 file_type VARCHAR(50), -- 文件类型 upload_time DATETIME, -- 上传时间 md5 VARCHAR(32) -- 文件 MD5(用于秒传)
-
异常处理
try {String[] result = FastDFSUtils.uploadFile(...);if (result == null) {throw new RuntimeException("文件上传失败");} } catch (Exception e) {log.error("文件上传异常", e);throw new BusinessException("文件上传失败,请稍后重试"); }
-
性能优化
// 1. 使用连接池(FastDFSUtils 已实现单例) // 2. 大文件使用异步上传 @Async public void uploadAsync(MultipartFile file) {// 异步上传 }// 3. 图片压缩后再上传 BufferedImage compressed = Thumbnails.of(file.getInputStream()).scale(0.5) // 缩小到 50%.asBufferedImage();
-
安全防护
// 1. 文件类型校验 String contentType = file.getContentType(); if (!contentType.startsWith("image/")) {throw new RuntimeException("只允许上传图片"); }// 2. 文件大小校验 if (file.getSize() > 10 * 1024 * 1024) {throw new RuntimeException("文件大小不能超过 10MB"); }// 3. 文件名校验 String fileName = file.getOriginalFilename(); if (fileName.contains("../") || fileName.contains("..\\")) {throw new RuntimeException("文件名非法"); }
🎓 总结
FastDFS 的核心价值
-
解决文件存储问题
- 文件与应用分离
- 支持海量文件存储
- 提供高性能访问
-
提高系统可用性
- 支持主从备份
- 自动故障转移
- 负载均衡
-
简化开发
- 提供简单的 API
- 支持 HTTP 直接访问
- 无需关心存储细节
下次开发时记住这些
- ✅ 复制 FastDFSUtils 工具类
- ✅ 配置 fdfs.properties(Tracker 地址)
- ✅ 实体类添加三个字段(orname、groupname、remotefilename)
- ✅ 表单设置 enctype=“multipart/form-data”
- ✅ 图片访问格式:http://IP:8888/group1/M00/00/00/xxx.jpg
关键概念回顾
概念 | 说明 |
---|---|
Tracker Server | 跟踪服务器,负责调度和管理 |
Storage Server | 存储服务器,实际存储文件 |
Group | 组,同组内服务器互为备份 |
FileID | 文件ID,包含组名和路径 |
StorageClient | 存储客户端,用于文件操作 |
MultipartFile | Spring 提供的文件上传接口 |
🎉 恭喜您!现在您已经全面掌握了 FastDFS 的使用方法和工作原理!
下次开发时,只需按照本文档的步骤操作,就能快速集成 FastDFS! 🚀