
网盘类项目在课程设计和毕业设计里一直很热门但很多同学拿出来的方案基本都长一个样Spring Boot MyBatis 做 CRUD文件直接丢本地磁盘或者塞进 MySQL 的 BLOB 字段再套个 LayUI / Vue 前端就完事了。这套东西交作业没问题但离“云存储”这三个字其实差得很远。我这次做的这个基于 Spring Cloud Hadoop SSM 的云存储网盘文件管理系统核心思路就是把存储层彻底换成 HDFS业务层用 SSM 拆分微服务让文件数据真正落到分布式文件系统里同时把秒传、分片上传、断点续传、分享、回收站这些网盘该有的能力补齐。这篇文章我会把整套系统的设计思路、核心实现、环境搭建和踩坑经验完整捋一遍适合正在做网盘 / 云存储类项目的同学也适合想搞懂 Spring Cloud 微服务怎么和 Hadoop 生态配合的开发者。用一句话概括这个项目MySQL 管业务元数据HDFS 管文件内容Spring Cloud 管服务治理SSM 管具体业务逻辑。这个分工是本项目最关键的设计决策后面所有代码和配置都是围绕它展开的。1. 为什么选这套组合Spring Cloud Hadoop SSM 的职责拆解先说结论这套技术栈不是为了堆名词而是把“网盘文件管理系统”这件事拆成了三个层次的问题每一层刚好对应一个技术体系。1.1 存储层为什么必须选 HDFS网盘的核心痛点其实是文件存储而不是文件列表。一个文件管理系统如果上线跑了几个月产生了几十万个文件你很快会遇到三个很现实的问题文件存本地磁盘一台服务器的磁盘迟早被打满扩容只能换更大硬盘成本和停机时间都受不了。文件存 MySQL 的 BLOB 字段数据库文件体积暴涨备份慢、查询慢而且大文件的读写会让 InnoDB 的缓冲池彻底失效整个系统的 SQL 性能都会被拖垮。文件如果存在应用服务器本地服务一重启、重新部署历史文件全丢完全不具备可靠性。HDFS 正好解决这几个问题。它的设计目标天生就是“存储海量大文件”默认块大小 128MB数据以多副本方式冗余存储默认 3 副本NameNode 管元数据、DataNode 管数据块数据节点挂掉之后系统会自动把缺失的副本补回来。对本项目来说不需要像 FastDFS 那样再单独维护一套 tracker 和 storage 集群HDFS 一台伪分布式机器就能模拟出“分布式存储”的效果还保留了将来横向扩展的可能性。更关键的是HDFS 把数据分成块这个块的概念正好可以支撑网盘的“分片上传、秒传、断点续传”。内容相同的大文件只要计算出相同的摘要HDFS 底层就能复用同一份数据块业务层只需要在数据库里新建一条引用关系这就是秒传的本质。1.2 SSM 和 Spring Cloud 各自扮演什么角色很多人看到 SSM 和 Spring Cloud 同时出现会有点懵觉得这俩是同一层次的东西。其实它们解决的是两个完全不同维度的职责SSMSpring SpringMVC MyBatis解决的是单服务内部的开发效率。Spring 管 Bean 生命周期和事务SpringMVC 处理 HTTP 请求路由和参数绑定MyBatis 把 SQL 和 Java 方法映射起来。在文件管理这种业务里用户表、文件表、分享表、回收站表的 CRUD 操作用 MyBatis 写 XML 比 JPA 更直观可控。Spring Cloud 解决的是多个服务之间的协作问题。当我把系统按业务边界拆成用户服务、文件服务、分享服务之后服务之间怎么互相找到地址请求怎么统一入口配置怎么集中管理服务挂了怎么熔断降级这些都属于 Spring Cloud 的范畴。在实际实现里我用的是 Spring Cloud Alibaba 体系下的 Nacos 做注册中心和配置中心Spring Cloud Gateway 做统一网关OpenFeign 做服务间调用。也就是说SSM 负责把每个微服务内部写扎实Spring Cloud 负责把服务之间的“路”修通两者并不冲突。1.3 这套架构的定位和适用范围这套组合特别适合两类场景一类是课程设计 / 毕业设计里要求“体现大数据技术元素”的项目比如标题里就点名 Hadoop那么存储层选 HDFS 是最稳妥的评审老师看到你的文件是真的落到 HDFS 上了而不是装个 Hadoop 摆样子分数档次完全不同。另一类是个人练手项目想搞明白微服务 分布式存储这套东西到底怎么串起来的。通过这个项目你相当于把 Spring Cloud 微服务治理、HDFS 分布式文件存储、SSM 业务开发、Vue 前后端分离这些技能点全部串了一遍比零散地看教程系统得多。需要说明的是如果项目只是为了快速跑通单机 Spring Boot 就够了如果追求“云存储味道”这套组合是性价比较高的选择既有技术深度实现复杂度又在可接受范围内。2. 系统整体架构设计与功能规划架构设计这件事最忌讳的就是一开始就闷头写代码。我在动手之前先把整个系统的服务边界、数据表、调用链路画了一遍确定不会出现“改一个功能要动三个服务”的情况才开始搭建工程。2.1 微服务模块怎么划分网盘类系统的业务边界其实很清晰我按照“不过度拆分”的原则把系统拆成了四个微服务和一个网关入口用户服务user-service负责用户注册、登录、鉴权、个人信息管理。核心是 JWT 令牌的签发与校验其他服务在网关层做统一鉴权后通过传递用户 ID 来识别身份。文件服务file-service这是最核心的服务负责文件上传、下载、文件列表、秒传、分片合并、回收站管理。它直接对接 HDFS 客户端同时操作 MySQL 里的文件元数据表。分享服务share-service负责文件分享链接的生成、取消分享、访问分享外链、转存文件。分享码用短字符串生成方便前端拼链接。存储服务storage-service做了一个很薄的 HDFS 封装层对外提供文件写入、读取、删除、判断存在的接口。之所以单独拎出来是因为文件服务和分享服务都有可能操作 HDFS避免重复代码也方便将来把 HDFS 换掉或加一层缓存。网关用 Spring Cloud Gateway 统一接收前端请求路由规则很简单/api/user/**打到用户服务/api/file/**打到文件服务/api/share/**打到分享服务。2.2 核心数据表设计思路数据表是整个系统的地基我按业务一个个拆核心的表就这么几张表名字段要点设计说明userid, username, password(BCrypt), nickname, created_time用户表密码必须加密不能明文存file_metadataid, file_md5, file_name, file_size, file_type, hdfs_path, upload_time文件实体表HDFS 只存一份物理文件多个用户引用同一条记录user_fileid, user_id, file_id, parent_id, file_name, is_dir, is_deleted, delete_time用户文件表描述“哪个用户拥有哪个文件/文件夹”支持目录树结构share_infoid, share_code, user_id, file_id, expire_time, visit_count分享表share_code 是唯一分享码chunk_uploadid, upload_id, file_md5, chunk_index, chunk_size, is_uploaded, create_time分片上传记录表用来实现断点续传这里面最关键的细节是file_metadata和user_file分离。一个文件本身只存一份在 HDFS 里file_metadata记录它的物理位置和 MD5 值user_file记录的是用户视角下的文件目录结构。这样两个人同时上传同一个文件MD5 相同HDFS 里只有一份数据两个用户各自关联一条user_file记录也就是说每个用户拥有的是同一个物理文件的“虚拟引用”存储成本直接减半。这个设计同时支撑了秒传。2.3 一次完整的上传请求走完哪些服务为了把服务间协作讲清楚我拿一次文件上传请求为例梳理一下完整链路前端调用网关/api/file/upload网关根据路由规则转发到文件服务。文件服务收到请求先从请求头解析 JWT得到用户 ID这个动作也可以在网关统一完成我这里选了网关做全局鉴权文件服务只信任网关传过来的用户头字段。文件服务计算文件 MD5先查file_metadata表看是否已存在相同文件。如果存在直接走秒传逻辑创建一条user_file记录返回“上传成功秒传”。如果不存在文件服务调用存储服务提供的writeFile接口底层用 HDFS 客户端把输入流写入 HDFS 对应路径。写入成功后文件服务往file_metadata插入文件实体记录再往user_file插入用户关联记录事务提交上传完成。下载请求的逻辑类似文件服务拿到用户文件 ID查到file_metadata里的hdfs_path再调用存储服务 Open HDFS 文件流包装成 HTTP 响应输出给前端。这套链路设计的妙处在于业务服务不关心文件字节是怎么分布的只关心“存到哪、取回来”HDFS 的细节全部封装在存储服务里。将来如果想把底层的 HDFS 换成 OSS 或 Ceph只需要改存储服务内部实现文件服务一行代码都不用动。3. 核心功能模块的实现细节功能模块是项目的主菜我重点讲几个“网盘系统没有这些功能就不好意思叫网盘”的核心点包括 HDFS 读写、秒传、分片断点续传、分享和回收站。3.1 HDFS 上传下载的完整代码流程HDFS 的 Java API 其实不难核心就是org.apache.hadoop.fs.FileSystem这个类。第一次接触的同学容易把 HDFS 操作写得很复杂实际上掌握三个方法就够了fs.create(Path)获取输出流把本地或内存里的数据写进 HDFS。fs.open(Path)获取输入流从 HDFS 读出数据。fs.delete(Path)删除文件。我封装在存储服务里的核心上传方法逻辑是这样的public String writeFile(InputStream inputStream, String fileName) throws Exception { // 1. 构建 HDFS 配置指定 NameNode 地址 Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://localhost:9000); // 2. 获取 FileSystem 实例这里用的是 HDFS 分布式文件系统 FileSystem fs FileSystem.get(conf); // 3. 构造存储路径按日期分目录避免单目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); Path hdfsPath new Path(/file-storage/ datePath / UUID.randomUUID() _ fileName); try (FSDataOutputStream out fs.create(hdfsPath, true)) { // 4. 用 IOUtils 工具类把输入流拷贝到 HDFS 输出流 IOUtils.copyBytes(inputStream, out, 4096, true); } // 5. 返回 HDFS 上的完整路径业务层存到 file_metadata 表 return hdfsPath.toString(); }注意这里有个细节我故意用了UUID.randomUUID()拼在文件名前面。原因是 HDFS 上是允许同名文件的不同目录下但同名容易造成混淆而且将来做下载时HDFS 路径上的文件名和用户展示的文件名最好是解耦的。用户看到的文件名存 MySQLHDFS 里的文件名只保证唯一就行。读取文件很简单核心就两行FileSystem fs FileSystem.get(conf); FSDataInputStream in fs.open(new Path(hdfsPath)); // 从这个输入流读数据写给 HttpServletResponse 的输出流 IOUtils.copyBytes(in, response.getOutputStream(), 4096, true);如果只是做课程设计写到这个程度就已经是“真正操作了 HDFS”的项目了不是摆设。但如果你想让文件服务再稳一点可以多考虑几个点流用完必须关闭、文件路径一定要做防目录穿越校验、上传的文件类型和大小要提前拦截。3.2 秒传和断点续传的实现方案秒传和断点续传是两个容易混淆的概念我先说清楚区别秒传指的是要上传的文件在服务器上已经存在那么客户端不需要真的传数据直接告诉服务器“我要引用那个已有的文件”瞬间完成。断点续传指的是一个文件很大网络中断后下次只需要传剩余的部分不需要重新整传。秒传的实现依赖 MD5。前端在文件选择后先计算整个文件的 MD5可以用 SparkMD5 这个库然后调用/api/file/check-md5接口把 MD5 发给后端。后端先查file_metadata表FileMetadata metadata fileMetadataMapper.selectByMd5(md5); if (metadata ! null) { // 秒传不写 HDFS直接建立用户文件关联 UserFile userFile new UserFile(); userFile.setUserId(userId); userFile.setFileId(metadata.getId()); userFile.setFileName(originalName); userFile.setParentId(parentId); userFileMapper.insert(userFile); return 上传成功秒传; } else { // 正常走上传流程 }这里就体现了第 2 章设计的file_metadata和user_file分离的好处没有这个分离秒传根本做不了。断点续传我用了“分片上传 合并”的方案。前端把文件切成固定大小比如每片 5MB逐片上传。每一片上传时后端记录分片序号和上传状态到chunk_upload表。当后端发现一个upload_id的所有分片都传完了就触发合并操作按分片序号把所有临时分片文件依次追加到一个最终文件里然后写入 HDFS。合并代码核心思路如下// 按 upload_id 查出所有已上传分片按 chunk_index 排序 ListChunkUpload chunks chunkUploadMapper.selectByUploadId(uploadId); chunks.sort(Comparator.comparingInt(ChunkUpload::getChunkIndex)); // 创建最终输出流 FSDataOutputStream out fs.create(finalPath); for (ChunkUpload chunk : chunks) { Path chunkPath new Path(chunk.getChunkTempPath()); FSDataInputStream in fs.open(chunkPath); IOUtils.copyBytes(in, out, 4096, false); in.close(); fs.delete(chunkPath, false); // 合并后删除临时分片 } out.close();这个方案的坑在于如果每片都走一次 HDFS 写入和删除会产生大量临时小文件而 HDFS 最怕的就是大量小文件。我的应对方式是限制分片数量最大 200 片并且合并后立刻清理临时文件甚至在hdfs-site.xml里开了dfs.blocksize较大值来缓解区块压力。当然更专业的做法是用 HDFS 的 append 功能直接往同一个文件追加但 append 对写并发有限制在这个项目里不划算。3.3 文件分享、回收站等业务功能网盘不能只有上传下载分享和回收站是用户感知很强的两个功能。分享的核心是生成一段短码。我的实现是用户选一个文件前端调用/api/share/create后端插入一条share_info记录生成一个 8 位短码。短码生成方式用 UUID 的前 8 位加 Base62 编码虽然极端情况下有碰撞可能但对课程设计项目来说完全够用。访问分享链接时网关把/s/{code}路由到分享服务分享服务根据 code 查到对应的文件信息校验是否过期然后跳转到下载接口。回收站的实现我建议用逻辑删除 定时清理而不是物理删除。user_file表里有is_deleted和delete_time字段用户点删除时只把is_deleted置 1回收站列表查is_deleted 1的记录永久删除时才真正调 HDFS 删除文件。定时清理我用 Spring 的Scheduled注解做每天凌晨三点扫描超过 7 天的回收站记录Scheduled(cron 0 0 3 * * ?) public void cleanRecycleBin() { ListUserFile expiredFiles userFileMapper.selectDeletedBefore(new Date(System.currentTimeMillis() - 7L * 24 * 3600 * 1000)); for (UserFile userFile : expiredFiles) { // 先查引用计数只有没有任何用户引用时才真正删 HDFS 文件 FileMetadata metadata fileMetadataMapper.selectById(userFile.getFileId()); int refCount userFileMapper.countByFileId(metadata.getId()); if (refCount 0) { storageService.deleteFile(metadata.getHdfsPath()); fileMetadataMapper.deleteById(metadata.getId()); } userFileMapper.deleteById(userFile.getId()); } }这里有个很容易忽略的细节删除 HDFS 文件前必须检查还有没有其他用户引用它。因为多个用户可能引用同一个物理文件某个用户从回收站彻底删除文件不代表这个物理文件可以删。这个“引用计数”的思路和 Linux 文件系统的硬链接很像不做这个检查就会出现“A 用户删文件导致 B 用户文件损坏”的事故。前端展示我用的是 Vue Element UI目录树用parent_id递归组装面包屑导航记录从根目录到当前目录的路径。文件图标按扩展名映射图片和文本文件可以走预览接口后端返回流前端开新窗口展示这些都是锦上添花的功能但你做了会整体显得完整很多。4. Hadoop 环境搭建与 Spring Cloud 整合实操这个部分我特别想重点写因为标题里的“Hadoop 伪分布式搭建”“从零开始安装 Hadoop”“Hadoop 集群搭建”这些关键词恰恰就是绝大部分人卡死的地方。很多人的项目代码写完了结果环境搭不起来或者 Hadoop 起不来项目直接废掉。4.1 伪分布式 Hadoop 环境从零搭建我在本地用 VMware 装了一台 Ubuntu 22.04 虚拟机也可以直接装在 WSL 里只要内存给够 4GB 以上就行。整个搭建过程分为五步每一步都有明显坑我挨个说清楚。第一步装 JDK 8配置环境变量。Hadoop 3.x 要求 Java 8 或 11我选 JDK 8因为后面 SSM 项目用 JDK 8 编译最稳。装完一定要在/etc/profile里写JAVA_HOME、PATH、CLASSPATH然后source生效java -version验证。第二步配置 SSH 免密登录。Hadoop 的守护进程NameNode、DataNode之间需要 SSH 无密码通信。执行ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys ssh localhost如果ssh localhost还是要求输密码99% 是.ssh目录权限不对把目录改成 700。这个步骤看起来不起眼但不配好后面start-dfs.sh会卡在输入密码上启动失败。第三步解压 Hadoop 包推荐去官网下载稳定版 hadoop-3.3.6解压到/opt/hadoop然后编辑四个配置文件。core-site.xml最关键指定 NameNode 地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configurationhdfs-site.xml指定副本数和目录configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/opt/hadoop/data/namenode/value /property property namedfs.datanode.data.dir/name value/opt/hadoop/data/datanode/value /property /configuration注意dfs.replication在伪分布式下必须设成 1否则只有一个 DataNode 却要写 3 副本会一直报块副本不足的告警。这个坑非常经典。第四步格式化 NameNode。第一次搭建必须执行hdfs namenode -format成功标志是日志里出现successfully formatted。这个命令只能执行一次后面如果再乱跑格式化会导致集群 ID 不一致DataNode 永远连不上 NameNode。第五步启动并在 Web UI 验证。执行start-dfs.sh然后jps看到NameNode、DataNode、SecondaryNameNode三个进程同时存在才算启动成功。浏览器访问http://localhost:9870Hadoop 3.x 的 NameNode Web 端口是 9870不是老教程里的 50070可以看到 HDFS 浏览界面这里能直观看到/file-storage目录下上传的文件。整个过程中最容易卡住的三座大山权限炸弹、格式化问题、端口占用。权限问题要用chown -R hadoop:hadoop /opt/hadoop把安装目录所有权给当前用户格式化问题只要记住“只在第一次启动前格式化”端口问题排查用netstat -ant | grep 9000看端口是不是被别的 Java 进程占了。4.2 Spring Cloud 注册中心与网关配置环境搭好之后开始搭 Spring Cloud 微服务骨架。我用的是 Spring Boot 2.7 Spring Cloud 2021.0.5 Spring Cloud Alibaba 2021.0.5.0这套版本组合比较成熟网上资料也多。注册中心选 Nacos 而不是 Eureka原因很简单Nacos 同时能做注册中心和配置中心一套服务解决两个问题而且中文文档丰富碰到问题容易排查。每个服务引入公共的注册中心依赖后在application.yml里配置spring: application: name: file-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: nacos网关的配置稍微多一点。Spring Cloud Gateway 是一个基于 WebFlux 的网关不能用传统 SpringMVC 那套阻塞式的写法它的核心是路由规则。我配置了三条路由spring: cloud: gateway: routes: - id: user-route uri: lb://user-service predicates: - Path/api/user/** - id: file-route uri: lb://file-service predicates: - Path/api/file/** - id: share-route uri: lb://share-service predicates: - Path/api/share/**, /s/**这里的lb://是关键语法它告诉网关走负载均衡从 Nacos 上找到对应服务实例。考虑到跨域问题我在网关里统一配置了 CORS后端服务不用再单独配跨域避免前后端联调的时候被浏览器同源策略折磨。网关还承担了全局鉴权的职责。我用一个 GlobalFilter 统一拦截所有请求放行登录和注册接口其它接口从请求头里取Authorization用 JWT 工具类校验签名并解析出用户 ID把用户 ID 放进请求头传给下游服务。这个方案避免了每个微服务都写一遍 JWT 校验代码真正的集中治理。4.3 SSM 整合 HDFS Java API 的踩坑点SSM 部分其实大家都熟Spring SpringMVC MyBatis 三件套在 Spring Boot 里被简化了主要就是 Mapper 接口扫描、事务管理、统一返回值和统一异常处理。我这里重点说和 HDFS 整合时踩过的两个坑。第一个坑是 Hadoop 依赖冲突。Hadoop 客户端会传递引入guava、jackson、protobuf这些老版本依赖而 Spring Boot 自带新版本两边的类冲突起来最常见的报错是NoSuchMethodError或ClassNotFoundError。解决办法是在 pom 里排除 Hadoop 传递的依赖只保留需要的核心 jardependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.6/version exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependency第二个坑是 Windows 下运行 Hadoop 客户端会提示缺少winutils.exe。解决办法很粗暴但有效去 GitHub 找一个对应 Hadoop 版本的winutils压缩包解压到某个目录然后在代码里设置System.setProperty(hadoop.home.dir, D:/hadoop-winutils);同时把bin目录里的winutils.exe和hadoop.dll复制到 JDK 的bin目录下这招能解决大多数本地调试环境下的 Hadoop 报错。如果还不行就老老实实把服务打成 jar 包丢到 Linux 虚拟机上跑生产环境不会出这种问题。第三个坑我放在这里一起说HDFS 默认开启了权限检查如果你用普通用户连 HDFS写入/file-storage目录会报Permission denied。课程设计环境里最简单粗暴的处理是关闭权限检查在hdfs-site.xml里加property namedfs.permissions.enabled/name valuefalse/value /property改完配置要重启 HDFS 才生效。这个操作在生产上当然不行但在本地学习环境和课程演示里属于常规操作不知道怎么关的同学经常被这个权限折磨一整天。5. 常见问题与排查技巧实录这个项目我前前后后调试了很久踩了很多坑有些坑光看报错信息根本猜不到原因。我把最有价值的排查经历整理成了三类当作速查表分享出来。5.1 环境类问题排查现象可能原因解决思路jps 里有 NameNode但 Web UI 打不开防火墙拦了 9870 端口宿主机用http://虚拟机IP:9870访问关掉 ufw 或放行端口NameNode 起来了DataNode 起不来日志报 ClusterID 不一致格式化过两次 NameNode删除 data 目录重新格式化之后绝不再格式化提交 jar 包运行报ConnectException: Connection refusedfs.defaultFS 配错或 Hadoop 没启动先jps确认进程再 ping 虚拟机 IP最后检查 9000 端口Windows 本地调 HDFS 报/tmp/hadoop-xxx/mapred不存在winutils 缺失设置hadoop.home.dir属性并补齐 winutils.exe环境问题有个共同点先看进程再看日志最后才改代码。很多同学一报错就先怀疑自己的 Java 代码其实大部分环境问题通过jps和tail -100 /opt/hadoop/logs/*.log就能定位。5.2 数据一致性与并发问题上传大文件时如果多个用户同时上传同一个文件可能出现两个人同时发现“文件不存在”然后同时写 HDFS产生两份物理文件。解决方法是给file_metadata表的 MD5 字段加唯一索引插入时捕获DuplicateKeyException捕获到就说明别人先写成功了当前请求直接改走引用逻辑。这算是一个非常经典的并发控制写法。另一个数据一致性问题是“HDFS 文件写成功了但 MySQL 事务回滚了”。比如用户上传完文件创建user_file记录时因为参数错误抛了异常MySQL 回滚了但 HDFS 里已经多了一个没人引用的文件成为孤儿文件。我的处理方式是在file_metadata表里加一个status字段上传时先写“上传中”业务事务提交改为“可用”然后写一个定期清理任务删掉超时未完成状态的文件记录和对应 HDFS 文件。这个思路其实就是把两个异构系统之间的操作变成最终一致虽然简单但很实用。5.3 性能优化与扩展建议项目跑通之后性能上明显能感觉到的问题是大文件上传慢、HDFS 小文件多、数据库连接池在高并发下不够用。我做了几处优化前端加了一个上传进度条用 XMLHttpRequest 的onprogress事件实时显示进度后端配合分片接口实现断点续传大文件上传体验提升非常明显。HDFS 客户端做成单例。FileSystem.get(conf)内部是带缓存的频繁创建会造成文件描述符泄漏我改成在 Spring 容器里维护一个FileSystem单例 Bean。SQL 层面给user_file表的(user_id, parent_id, is_deleted)建组合索引目录树查询从几百毫秒降到几十毫秒。数据库连接池从 Tomcat JDBC 换成 HikariCPSpring Boot 默认自带只要配置好最大连接数就行。注意 MySQL 的max_connections也要够大两边不匹配会导致连接池疯狂重试。再往后扩展的话有几个方向可以做文件预览服务图片缩略图、视频转码可以引入 FFmpeg、回收站定时清理、分享链接接入 Redis 缓存访问计数、网关层加 RateLimiter 限流、HDFS 文件加压缩Snappy。这些属于进阶功能做不做取决于你的时间预算但思路和方案都是现成的按需实现即可。最后再分享一个实操中体会比较深的小技巧在开发阶段不要把 HDFS 的地址写死在代码里。我用application.yml里的一个自定义配置项管理fs.defaultFS本地连 Windows 的假 Hadoop 环境时改成hdfs://localhost:9000部署到虚拟机时改成hdfs://192.168.x.x:9000切换环境只改配置不碰代码。类似的Nacos 地址、MySQL 地址全部外置到配置中心管理整个系统迁移环境会从容很多。从我反复折腾的经历看这套项目的难点从来不在某个 API 怎么调而在于把 Hadoop、微服务、业务代码三个层次正确衔接起来只要架构分层清晰每一层的问题都能独立定位和解决。