最近在开源社区闲逛,看到个项目标题很有意思:“对标百度网盘:基于SpringBoot开发的分布式文件系统”。一开始我心想又是那种简历项目,代码没多少,吹牛占一半。但clone下来跑了一圈之后,我得承认,这个项目做得比我预期完整:分片上传、秒传、断点续传、文件分享、在线预览、团队成员权限管理,后端主体一个SpringBoot工程就撑起来了,没有一上来就拆二十个微服务,也没有那种让你装Elasticsearch全家桶的负担。
这篇文章不打算写成项目文档翻译,而是从一个用过不少网盘系统、也自己维护过文件服务的开发者的角度,把这个分布式文件系统拆开揉碎聊一聊:它为什么这么设计、关键功能是怎么实现的、部署过程中有哪些坑、以及我在跑通之后的一些反思。适合想自建私有网盘的团队,也适合想学习分布式文件系统实现原理的Java后端开发者。
1. 项目定位:SpringBoot能做什么样的网盘
1.1 为什么还要造一个网盘轮子
现在市面上商业网盘已经很成熟,容量大、跨端同步、分享方便,但不少人心里还是有芥蒂:限速、隐私、审核、账号体系绑死。自建网盘的需求一直存在,尤其是团队内部想控制数据、想要更高传输速度、想给客户交付一套私有化知识库的时候,商业网盘显得不那么可控。
开源网盘项目其实也不少,但常见的情况是两种:一种是老牌的PHP项目,功能全但代码风格停留在十年前,想改个逻辑要顺藤摸瓜找半天;另一种是微服务全家桶,网关、认证、文件服务、消息队列、监控全给你安排上,部署文档翻到第三页就开始劝退。
这个SpringBoot项目显得比较务实。它把网盘核心功能都集中在单个应用里,存储层做了抽象,既可以存本地磁盘,也能对接S3协议的对象存储。对Java团队来说,这种技术栈的学习成本低很多,源码结构也清晰,拿来做二次开发或者研究都很合适。
1.2 整体架构与核心存储设计
整个系统的架构不复杂,但每层职责分得很清楚:
- Nginx层负责负载均衡、静态资源代理、上传请求的反向代理。
- SpringBoot应用层承担业务逻辑:用户认证、文件元数据管理、分片上传、分享链接、在线预览。
- MySQL负责保存文件元数据、用户信息、分享记录、回收站记录。
- Redis用于处理秒传索引、分布式锁、上传分片记录、热点数据缓存。
- 文件实体数据存放在本地磁盘或MinIO这类S3对象存储中。
用一个句式概括就是:逻辑数据放MySQL,索引数据放Redis,文件内容放磁盘/对象存储,应用层通过统一存储接口访问。
我特意看了一下文件元数据表的设计,核心字段大致包括:文件ID、文件名、文件MD5、文件大小、存储路径、存储类型、上传者ID、引用计数、状态。注意它保留了“引用计数”这个字段,这是实现秒传和回收站不误删的关键,后面会详细说。
另外,存储层的接口也做了抽象,本地存储和S3存储分别有对应的实现类,新增一种存储后端只需要实现统一的读写接口。这种“面向接口设计”的方式,让单体项目也能轻松替换底层存储,不至于绑定在某个具体方案上。
1.3 为什么选单体而不是微服务
这里我要多说一句。很多人看到“分布式文件系统”这六个字,下意识觉得必须上微服务,各节点独立部署、独立扩容才算分布式。但这个项目给了一个更接地气的思路:先把功能做完整,部署上通过Nginx对多个应用节点做负载均衡,Redis和MySQL共用,存储层用对象存储,这样从外部看它已经具备水平扩展能力,但应用内部还是清晰的单体结构。
单体应用最大的好处是调试方便、事务边界简单、部署运维负担轻。一个中小团队自己用,单节点就能扛住日常流量;如果后续并发上来了,加机器、挂Nginx,应用层无状态可以横向扩容,数据库再加读写分离或缓存优化。这种演进路径是平滑的,而不是一开始就把所有复杂度都装上。
2. 核心功能设计与关键实现
2.1 分片上传和秒传的完整链路
网盘功能里最核心也最容易翻车的,就是大文件上传。如果直接把一个1GB文件所有字节一次性POST给后端,内存和带宽都受不了。这个项目用的是前端分片 + 后端合成的方案,整个流程可以拆成四步:
第一步:前端在本地把文件按固定大小切割,常见做法是5MB一片,同时计算整个文件的MD5。
第二步:调用上传准备接口,把文件MD5、文件名、总大小、总片数发给服务端。服务端先拿着MD5去文件元数据表里查,如果这个文件已经存在并且数据完整,就直接复用已有的文件记录,把引用计数加一,返回“秒传成功”。
第三步:如果文件不存在,服务端创建一个上传任务,返回批次号和下一片要上传的序号。前端再逐片调用上传接口,把分片数据写到临时目录,并把已经上传完成的分片序号记录到Redis。
第四步:所有分片传完后,前端调用合并接口。服务端校验分片数量,把所有临时分片按顺序合并成完整文件,再重新计算一次MD5,与前端传上来的MD5做比对,全对上了才写入正式文件记录。
这套链路最巧妙的地方,是把“秒传”和“分片上传”统一到同一个MD5体系里。只要你团队里有人上传过这个文件,后面任何人上传同一份文件,都会直接秒传成功,前端看起来像瞬移,实际上后端只增加了一条元数据和一次引用计数。
我当时看源码的时候,发现准备接口里还有一个小细节:如果之前的某次上传任务因为网络中断没完成,Redis里的分片记录还在,服务端会返回“已上传分片序号列表”,前端只需要从断掉的那一片继续传。这个就是断点续传的底层支持,不需要额外做复杂的状态机。
2.2 分片合并如何保证文件不损坏
分片合并看起来简单,把每片按顺序拼起来就行,但实际写过的人会知道,这里有好几个容易踩的坑。
文件写入方式很关键。假设分片大小固定5MB,最后一片可能不足5MB,合并时不能简单地把所有分片文件拼起来,而是要用RandomAccessFile按偏移量写入,起始位置是当前分片序号乘以分片大小,这样即使分片乱序到达,也能准确落到自己的位置。源码里我看到它对每个上传任务持有一个锁,防止同一个任务的分片并发合并时互相覆盖。
分片文件的命名也很有讲究。我见过有项目直接用原始文件名作为临时分片名,结果同一文件多人并发上传时,分片互相覆盖,最终合并出来的文件缺了一块。这个项目把临时分片命名成“任务批次号-序号”,从根上隔离了并发冲突。
还有一个容易被忽略的点:合并完成之后要做一次整体MD5校验。并发环境里,客户端传上来的MD5不一定可靠,网络丢包、前端脚本bug都可能造成数据不一致。合并后重新计算MD5,与客户端上报值比对,不一致就直接丢弃并让客户端重新走上传流程。这个校验动作多花几秒钟,但能避免用户下载到损坏文件后回来骂人。
2.3 文件分享与权限控制
对标百度网盘,分享功能是绕不开的。这个项目实现了提取码分享、链接有效期、下载次数限制三个维度,核心逻辑在分享服务里面。
创建分享时,系统生成一个短码作为文件链接标识,同时生成一个随机提取码。分享记录的访问逻辑是:访问链接 -> 校验分享是否存在且未过期 -> 输入提取码 -> 校验一致后下发临时访问凭证 -> 读取文件流返回。
权限控制的重点在于:一旦某个用户获得了分享链接,系统只给这个用户“读”的权限,不会让用户看到文件所在的真实存储路径,下载时也不允许设置断点续传以外的额外操作。这种轻量级鉴权方式对网盘应用来说足够了,不用引入复杂的工作流引擎。
在线预览也是我比较关注的功能。源码里使用了Spring的ResourceRegion来支持HTTP Range请求,这样视频文件可以拖动进度条,PDF和图片按需加载,预览的时候不会一次性把整个大文件读进内存。这个实现比较加分,很多自建网盘做到最后,能上传能下载,就是预览拉胯,而这个项目的预览体验基本能追上商业网盘。
2.4 回收站、引用计数与异步清理
删除文件不能像删数据库记录那样直接DELETE掉。网盘里常见的坑是:同一个文件被多个用户的文件夹引用,A用户删除了,B用户那边的文件也莫名其妙没了。这个项目用引用计数来解决。每上传一份文件,对应的人引用文件记录,引用计数加一;删除只是把文件状态标记为回收站状态;回收站里的文件30天后被清理时,引用计数减一,只有引用计数归零,物理文件才会被真正删除。
这个设计的价值在于:秒传功能带来的是同一物理文件被多个逻辑引用,而回收站和引用计数保障了这些引用之间互不干扰。定时清理任务会把到期文件从回收站彻底删除,但不会误伤仍被其他用户引用的文件。
异步清理我特意加了一个自己的验证:把物理文件所在目录的权限改成不可读,然后观察清理任务是否报错。结果发现定时任务只负责删除,没有任何前置检查,这在实际生产环境是有点危险的。后来我给清理任务加了一个“该文件是否还有有效上传任务引用”的判断,避免把一个正在上传的临时文件当作垃圾清掉。
3. 项目部署与实操配置
3.1 从零开始跑通系统
部署之前先列一下环境依赖,这个项目不复杂,但也不要瞎装版本:
- JDK 1.8或者更高,建议直接用JDK 17,省得后面遇到Spring Boot版本兼容问题。
- Maven 3.6+,用于构建项目。
- MySQL 5.7+,存储元数据。
- Redis 6.0+,存储秒传和分片记录。
- MinIO可选,如果你不想用本地磁盘模式,就装一个MinIO。
首次启动我建议先走本地磁盘模式,这样可以少一个组件,快速验证功能。步骤很简单:
git clone <项目地址> cd <项目目录> mvn clean package -DskipTests然后准备数据库,把项目里doc目录下的初始化SQL导入MySQL,创建好库和表。接着修改配置文件的数据库连接和Redis连接,最后启动:
java -jar target/xxx.jar启动日志里出现“Started XxxApplication”就说明起来了。默认端口一般是8080,前端项目如果是要单独部署的,记得把静态资源丢到Nginx,前端请求接口时走反向代理。
我本地跑通整个流程大约花了半小时,最大的时间消耗在等Maven下载依赖,真正启动过程很快。如果你发现启动报错,八成是MySQL连接串时间时区不对,或者Redis没开,这两点优先排查。
3.2 文件存储路径与配置参数
本地磁盘模式下,系统会把上传文件写到配置的存储目录里。配置文件里有几个关键参数值得单独说明:
file: storage: type: local local: base-path: /data/clouddisk/files s3: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: clouddisk region: us-east-1type字段控制使用本地还是S3,切换存储类型只需要改这个值,然后补充对应配置,不需要改代码。这个设计我在后面实际扩存储的时候才感受到有多方便,一台服务器磁盘快满的时候,换个桶配置就行,业务无感知。
还有一个容易踩的坑是Nginx上传大小限制。如果你前端直连Tomcat,Spring Boot默认限制单个请求最大10MB。分片上传虽然单片通常不大,但秒传校验、合并请求的请求体也可能超过默认值。建议在Spring Boot配置里调大限制,同时Nginx也要放开client_max_body_size,否则你会看到明明一切正常,上传到一半直接442。
在Nginx配置里加上:
client_max_body_size 10g; proxy_request_buffering off;proxy_request_buffering off是我后来才发现的,它的作用是让代理服务器不把整个请求体先缓存到磁盘再转发给后端,对大文件传输和内存占用都更友好。
3.3 对接MinIO对象存储
如果要做多节点部署,本地磁盘模式有一个明显短板:每个应用节点只能看到自己本地的文件,别人上传的文件在自己的节点上读不到。这时候就要上对象存储。
MinIO部署起来很简单,下载二进制、跑起来,然后用客户端创建一个bucket,把上面配置文件里的type改成s3,填好地址和密钥,重启项目,上传文件后你会看到MinIO里多出了对应对象。
我在实验环境里同时跑两个SpringBoot应用节点,共用同一个MinIO桶,前端随机请求不同节点的接口,不管文件上传到哪个节点,另一个节点都能正常下载和预览。这就是存储层抽象带来的能力:应用无状态,文件不落本地,水平扩展变成了加机器的事。
不过要注意,对象存储和本地磁盘在“秒传”场景下有个细微区别。本地磁盘模式可以直接引用同一个物理文件路径,而对象存储里的同一个对象本质上已经是多引用复用,所以元数据表里存的对象key是一样的。恢复文件时不能简单重命名,要善用对象存储的版本控制功能,避免误覆盖。
4. 常见问题与排查技巧实录
4.1 秒传索引失效:重启后所有文件都要重新传
我第一次测试的时候,上传了一堆文件,秒传功能正常。然后我重启了一下Redis,再点击上传同一个文件,发现前端又老老实实传了一遍完整文件。
排查下来发现原因很简单:秒传索引除了MySQL里的文件记录,还在Redis里存了一份“文件MD5 -> 文件ID”的高速映射,如果这个映射丢了,系统就只能先走完整上传链路,合并完成后再发现文件已经存在,这时候再做去重也晚了。
解决方法是双保险。Redis这边开启AOF持久化,配置appendfsync everysec,这样即使重启,最多丢一秒内的数据。MySQL那边也要保证文件记录不丢,定时任务定期把Redis里的秒传索引重新刷一遍。更稳妥的做法是,上传准备接口发现Redis命不中时,再去MySQL查一次,查到了就回填Redis,没必要全量重建。
有一点我需要提醒:如果项目部署了多个应用节点,Redis必须使用同一个实例或者集群,不能用每台机器单独的内存Map。一旦每台节点各持一份索引,A节点秒传成功,B节点上还是老样子,体验割裂得很明显。
4.2 上传大文件时内存溢出
这个项目原生实现里,上传接口接收MultipartFile时,中间会经过内存缓冲区。默认情况下Tomcat对文件上传请求的解析会把小文件放在内存,超过阈值才写临时目录。如果你的服务器内存本身就紧张,上传一个几百MB的文件,可能直接OutOfMemoryError。
我的建议是改动两处:
第一,调大Tomcat写临时目录的阈值,让文件尽早落到磁盘而不是内存。在配置里可以设置:
spring: servlet: multipart: max-file-size: 512MB max-request-size: 512MB file-size-threshold: 2KBfile-size-threshold设置为2KB意味着几乎任何上传文件都会走磁盘缓冲,而不是堆内存。这能显著降低大文件上传时的内存压力。
第二,如果你有精力和权限去改代码,尽量把上传改成流式处理,从InputStream里直接读到目标存储,而不是在Controller层拿到一个完整MultipartFile对象。这个优化会触及前后端协议,要谨慎做,但一旦做完,内存占用曲线会好看很多。
4.3 分片合并后校验失败,文件总差几个字节
我一直觉得分片合并是网盘系统里最需要警惕的环节。有次实验,我模拟了网络抖动场景,故意把一片文件内容截断,结果整个合并过程没有报错,文件也生成了,但MD5校验不过。
查问题的思路是这样的:先检查Redis里记录的分片列表,看序号有没有缺失;再看磁盘上每个分片的大小是否和前端上报一致。最后发现前端在上传最后一片时,因为连接中断发起重试,重试时候后缀被改了,导致磁盘上出现两个相同序号的正常分片和一个不完整的残片,合并时取到了残片。
解决方式很笨但很有效:服务端在接收每片上传时,额外记录该分片自己的MD5。合并之前把所有分片的MD5列表捞出来,和预期值做一次全量比对,任何一个分片不匹配就直接清空重传。这个操作会多一点点IO开销,但能保证不产出损坏文件。
4.4 在线预览一直404
预览功能要正常工作,除了后端要正确返回文件流,前端请求的URL也必须能命中真正的文件控制器。我在源码中发现预览视图是一个自定义Controller,很多情况是Spring Boot的默认静态资源处理器抢先拦截了路径,或者前端请求的资源路径和Controller映射对不上。
排查预览404时,先打开浏览器开发者工具,看预览请求实际返回的状态码和响应头。如果是404,看是不是代理层把路径重写了。如果返回的是200但页面空白,则多半是Range响应头缺失,导致播放器拿不到完整视频分段。
还有一个小坑是文件名含特殊字符,比如空格、中文、#号。这类字符在URL里必须做编码,否则后端解析文件名时拿到的是一串乱码,自然找不到文件。我处理的时候在下载接口里统一对文件名做了URLEncoder处理,前端也把文件名编码后拼进URL。
4.5 定时清理把正在上传的临时文件删了
定时任务是网盘系统里最容易闯祸的后台角色。回收站清理逻辑如果只盯着到期时间,很容易把正在上传中的临时文件也当成垃圾。我在一个多用户并发测试中亲眼看过:用户传了个大文件,还没传完,后台另一个定时任务把临时分片目录里的文件删了一半,上传任务当场失败。
修复思路是给上传任务表加上“最后更新时间”字段,清理任务只处理更新时间超过2小时且状态仍为“上传中”的任务。这样可以有效区分“正在活跃上传”和“因异常中断的僵尸任务”。如果你不想改表结构,也可以在Redis里维护一个活跃上传任务ID的集合,清理前先判断任务ID是否在集合中。
4.6 问题排查速查表
| 现象 | 常见原因 | 首选排查方式 |
|---|---|---|
| 秒传失效 | Redis未持久化/索引丢失 | 检查Redis AOF配置,回填MySQL索引 |
| 大文件上传OOM | MultipartFile占用内存过多 | 调大file-size-threshold,改流式处理 |
| 合并后MD5不一致 | 分片覆盖/残片混入 | 记录每片MD5,合并前全量比对 |
| 在线预览404 | URL映射冲突/特殊字符 | 检查代理路径与Controller映射 |
| 清理任务误删文件 | 未区分上传中任务 | 加入活跃任务判断或更新时间门槛 |
| 跨节点下载失败 | 本地磁盘模式不支持多节点 | 切换S3存储,让文件落对象存储 |
5. 个人实测总结与扩展方向
5.1 这个项目适合谁去读
如果你是想快速搭一套私有网盘给团队内部用,又不想引入太重的基础设施,这个项目值得花一天时间跑通。它覆盖的技术栈很典型:SpringBoot业务代码、MySQL元数据管理、Redis高性能索引、S3对象存储、Nginx反向代理,这套组合在中小规模团队里非常实用。
如果你想学习分布式文件系统的关键机制,也可以把它作为入口。先读秒传和分片上传这部分,理清文件引用关系;再看存储抽象层,理解怎么在不同类型存储之间切换;最后看回收站和定时任务,体会生产环境里“删除”为什么不是只删一条记录。
5.2 后续功能还能怎么扩
项目当前已经很完整,但要继续对标商业网盘,还有三个方向值得扩展:
第一个是离线下载功能。用户粘贴一个HTTP链接,服务端后台任务去拉取文件,这需要增加一个任务队列机制,当前项目里这块是空的。
第二个是WebDAV协议支持。有了WebDAV,用户可以直接挂载成电脑磁盘或者用第三方客户端管理文件,体验会提升一个档次。Spring Boot生态里有很多WebDAV库可以对接,不需要从零实现。
第三个是全文检索。现在在线预览支持文档,但还不支持搜文件内容。如果要上全文检索,最容易扩展的方案是给文件内容做异步抽取,把纯文本或者PDF里的文字送进全文索引服务,再提供关键词搜索接口。
5.3 最后说点实在话
我在跑通这个项目后,最大的感受是不要被“分布式文件系统”这几个字吓到。真正的分布式存储底层确实复杂,但在应用层做一个团队规模的网盘系统,用好SpringBoot、MySQL、Redis、对象存储这四件套,已经能覆盖绝大多数使用场景。
如果你也要基于这类项目做二次开发,我建议把数据安全放在第一位:数据库要定期备份,Redis要开启持久化,存储桶要开版本控制。功能可以慢慢补,数据一旦丢了,再好的功能也救不回来。先跑通最小集,再逐步加功能,这个思路在网盘系统的落地过程中永远不会错。