说实话,选“分布式存储平台”这个题目的时候,我根本没预想到后续整个开发周期会这么长。现在回头看,这个题目最坑人的地方不是算法写不出来,而是很多人拿到项目源码之后不知道该怎么把它跑起来:数据库脚本怎么导、多个存储节点怎么启动、Nginx怎么转发,全凭感觉来,错了就卡半天。这篇文章我打算把毕业设计从选题定位、架构设计、环境部署、核心源码、数据库设计到常见问题排查,完整地捋一遍,给正在做同款题目的同学一条可以直接照做的路线。
分布式存储平台这个概念,听着很唬人,实际落到毕设里要做的事情非常明确:用户上传的文件不是只存在一台机器上,而是被拆成多个部分分散存到若干台存储节点,管理节点负责统一调度和索引。这样做解决了单机容量上限、单点带宽瓶颈和数据丢失风险三个核心问题。毕设做到这个程度,已经完全能满足“设计+实现”的验收要求,也够你在面试时当成一个正经项目来聊。
这篇内容适合正在发愁毕业设计怎么落地的本科生,适合想拿Java后端项目练手但是缺完整项目经验的人,也适合想快速搭建一套“多模块+集群+部署”演示系统的转行朋友。我会按照“选题定位→技术选型→环境部署→源码解读→数据库设计→问题排查”的顺序展开,每一步都会讲清楚背后的原因,不是简单给命令。
提示:下文提到的项目结构和部署方案,基于这个选题最主流的Spring Boot多服务架构展开。如果你手上版本的存储节点用了Python或其他语言实现,部署思路完全同理,把对应步骤替换掉就行。
1. 选题定位与总体架构:先把“要做什么”想清楚
1.1 单机存储的瓶颈与这个毕设的切入点
所有分布式系统的存在理由,都可以归结为一句话:单点扛不住了。毕设里的分布式存储规模远达不到工业级,但必须把核心矛盾体现出来——当一台存储服务器容量撑满、带宽打满时,除了换更大的机器,还有没有别的解决方案?分布式存储给的答案是:加机器,而且让加进来的机器自动分担压力。
所以平台的核心流程被设计成了这样:
- 用户通过管理节点上传文件;
- 管理节点根据文件ID计算一致性哈希,确定该文件归哪个存储节点管;
- 文件被切分成多个分片,依次写入目标存储节点,副本策略同时写其他节点;
- 存储节点落盘并返回块信息,管理节点把元数据和索引写入数据库;
- 下载时,管理节点根据文件ID查询元数据,拿到分片所在节点,再并发拉取分片合并返回。
这条链路走通,你的毕设核心就已经完成了。听起来不难,但里面每一个环节都对应一个可考查的知识点:哈希路由、分片策略、副本一致性、元数据管理,这些正是答辩老师最关心的内容。
1.2 功能清单与验收点
我建议在做任何代码之前,先把功能拆成清晰清单,后面开发和测试都照着它走。下表是这套平台的核心功能集合:
| 模块 | 功能点 | 验收标准 |
|---|---|---|
| 用户模块 | 注册、登录、退出 | 登录后才能上传和下载文件 |
| 文件上传 | 分片上传、合并 | 大于50MB的文件能正常完整上传 |
| 文件下载 | 分片并行下载、合并输出 | 下载文件与源文件MD5一致 |
| 文件管理 | 文件列表、删除、回收站 | 删除进入回收站,可恢复 |
| 节点管理 | 节点注册、心跳、状态展示 | 停掉一个节点,管理端能看到离线 |
| 系统统计 | 节点容量、文件总数 | 首页图表展示 |
验收标准不是我随便写的,它们直接对应答辩演示时的关键步骤。比如MD5一致性,演示时拿一个压缩包传上去再下载下来,解压运行一次,比任何图表都有说服力。
1.3 整体架构与一次完整的上传过程
这套系统在物理部署上分成三个角色:
- 前端/客户端:Web页面,提供上传下载界面;
- 管理节点:核心服务,负责鉴权、元数据管理、路由计算、任务调度,对外提供REST接口;
- 存储节点:可以启动多个实例,负责真正的文件落盘读写,接收管理节点下发的写入和读取指令。
部署形态上,Nginx作为唯一的入口反向代理到管理节点;管理节点和存储节点之间通过注册中心和心跳机制维护状态。存储节点可以部署在同一台机器上用不同端口模拟,也可以拆到多台虚拟机上。
拿一次完整上传举例,时序上大致是这样的:用户登录获取Token→上传文件到管理节点→管理节点将文件切分为512KB大小的分片→根据文件ID哈希确定目标节点集合→将分片分发到对应存储节点→存储节点返回块ID→管理节点将所有映射关系写入数据库→返回“上传完成”给前端。
整体思路清楚之后,下一步就要把开发环境和部署环境准备好。
2. 技术选型与环境依赖:把部署前的坑先趟一遍
2.1 技术栈为什么这么选
毕设技术选型和工业项目不同,不是追求最新最热,而是要保证三个点:能跑通、你能讲清楚、环境好搭建。身边走了不少弯路的人,一上来就上K8s、上分布式数据库,结果部署环境搞了两周还没起服务。我自己最终采用的是非常经典的组合:
| 技术 | 用途 | 选择理由 |
|---|---|---|
| Java + Spring Boot 2.7 | 管理节点和存储节点的后端框架 | 生态成熟,资料最多,出问题好搜 |
| MySQL 8.0 | 元数据和索引存储 | 学校普遍会教,部署简单 |
| Nginx 1.24 | 反向代理和负载均衡入口 | 配置直观,能体现网络层面的知识 |
| Redis | Token缓存、热点文件缓存 | 加分项,非必需 |
| Maven | 项目构建和依赖管理 | 团队标配 |
| Hutool / FastJson | 工具库和JSON处理 | 提升开发效率 |
| OSS(可选) | 云存储对接演示 | 如果你想体现混合云理念 |
为什么不用最新的JDK 21、Spring Boot 3.x?不是不能用,而是很多高校机房、服务器环境还停留在JDK 8,Spring Boot 3强制要求JDK 17以上,一旦环境不匹配光编译报错就能劝退一半人。稳妥优先,我自己用的是JDK 1.8 + Spring Boot 2.7.18这一套黄金组合。
2.2 部署环境的最低要求
在部署之前,先把依赖环境准备好。下面是这个项目能够正确运行的最低标准环境:
- JDK 1.8(64位)或更高
- Maven 3.6及以上,用来编译打包
- MySQL 8.0(5.7也兼容,建议8.0)
- Nginx 1.20及以上
- Linux服务器或本机Windows均可,不建议用32位系统
- 内存至少4GB(管理节点+两个存储节点实例同时跑会比较吃内存)
如果是在本机Windows上做演示,建议用IDEA直接跑服务;如果要部署到服务器,建议用打包成jar的方式,下面第3章会两种都说。
2.3 拿到源码后的项目结构解读
一个典型的分布式存储毕设项目,Maven结构大概长这样:
distributed-storage-platform/ ├── admin-server/ # 管理节点 │ ├── src/main/java │ │ ├── controller/ # 对外REST接口 │ │ ├── service/ # 路由、元数据、任务调度等核心逻辑 │ │ ├── mapper/ # MyBatis数据访问层 │ │ └── config/ # 全局配置、跨域、拦截器 │ └── src/main/resources │ ├── application.yml │ └── mapper/ # XML文件 ├── storage-node/ # 存储节点 │ ├── src/main/java │ │ ├── controller/ # 接收管理节点下发的读写指令 │ │ ├── service/ # 本地磁盘读写、分片合并 │ │ └── client/ # 与管理节点的心跳通信 │ └── src/main/resources │ └── application.yml ├── common/ # 公共模块(实体类、工具类、常量) ├── sql/ │ └── init.sql # 初始化脚本 └── docs/ # 说明文档和接口文档这个结构一眼看上去就一个“管理端+多个存储端+公共模块”的思路。你在部署前一定要先看清自己拿到的代码是什么结构,不要上来就找启动类,先找到根目录的pom.xml,确认子模块都列全了再继续。
2.4 部署前最重要的三件事
动手部署前,有三件小事必须提前搞定,否则后面所有步骤全是坑:
- 确认Java与Maven版本:命令行分别敲
java -version和mvn -version,认准是1.8和3.6以上。 - 准备数据库账号:建一个专门的库和账号,不要用root裸奔,后面连接配置要写清楚。
- 检查端口占用情况:管理节点默认8080,存储节点默认8081、8082,Nginx监听80。在Linux上用
netstat -tlnp看一眼,被占用的端口提前换掉。
这三件准备工作做完之后,进入真正的部署流程。
3. 从源码到可运行:部署实操全流程
3.1 第一步:初始化数据库
打开sql/init.sql,看一下内容,确认里面包含建库、建表的语句。然后在MySQL里执行:
mysql -uroot -p < sql/init.sql如果你用的是Navicat或DataGrip,直接打开脚本文件然后运行也可以。执行完毕后,可以使用下面的命令快速确认表建好了:
USE distributed_storage; SHOW TABLES;正常会看到像tb_user、tb_storage_node、tb_file_metadata、tb_file_block、tb_file_copy这样的表。注意一点:初始化脚本不是单纯建表,里面通常还会写入两条默认存储节点记录和测试用户数据,这为后面节点管理页面的显示提供了初始数据,别删掉。
3.2 第二步:管理节点的配置与启动
先打开admin-server/src/main/resources/application.yml,重点关注如下几项配置:
server: port: 8080 # 管理节点对外端口 spring: datasource: url: jdbc:mysql://localhost:3306/distributed_storage?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: your_user # 改成你自己的数据库账号 password: your_password # 改成你自己的数据库密码 storage: node-check-interval: 10000 # 存储节点心跳检测间隔,单位毫秒 replica-count: 2 # 默认副本数用户名和密码一定要和3.1步创建的一致,时区参数serverTimezone=Asia/Shanghai也建议保留,否则可能出现时差报错。改好配置后,两种启动方式:
方式一:IDEA直接运行。在IDEA中打开根目录pom.xml,等待Maven依赖下载完成后,找到AdminServerApplication直接右键运行。
方式二:打包成jar。在项目根目录执行:
mvn clean package -DskipTests然后在admin-server/target/下找到生成的jar包,执行:
java -jar admin-server-1.0.0.jar启动日志出现类似Tomcat started on port(s): 8080的字样,说明管理节点已经起来了。Windows下我用的是方式一,Linux服务器上用的是方式二,两种都很稳定。
3.3 第三步:存储节点的多实例启动
存储节点是核心中的核心,它决定了你的系统到底“分布式”在哪里。
打开storage-node/src/main/resources/application.yml,配置如下:
server: port: 8081 # 每个存储节点的端口必须不同 node: id: node-01 # 节点唯一标识 ip: 127.0.0.1 # 本机地址,部署到多台机器时改成对应IP store-path: /data/storage/node-01 # 文件落盘目录,提前创建好 max-capacity: 10GB # 上报容量,用于管理端展示 admin: register-url: http://127.0.0.1:8080/api/node/register如果要启动第二个节点,可以把server.port改成8082,node.id改成node-02,store-path改成另一个目录。注意每个节点的落盘目录一定要不同,否则两个节点互相覆盖文件。
如果你想在IDEA里同时启动多个存储节点,可以编辑运行配置,在VM options里加上-Dserver.port=8082覆盖默认端口,就不用复制多份配置文件了,这个技巧很实用。我在部署时开了3个节点,分别是8081、8082、8083,目录分开,跑得很稳。
存储节点启动后,会自动调用管理节点的注册接口完成注册,之后每隔10秒发送一次心跳。管理端网页的“节点管理”页面,能看到所有在线节点,包括IP、容量、状态。
3.4 第四步:Nginx反向代理与负载均衡配置
Nginx在这里承担两个职责:一是作为外部的统一入口,二是将读写请求合理分流到管理节点。一个经过验证的配置片段如下:
upstream admin_backend { server 127.0.0.1:8080; } server { listen 80; server_name localhost; # 上传大小限制,默认只有1MB,必须调大 client_max_body_size 100m; location /api/ { proxy_pass http://admin_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root /opt/distributed-storage/dist/; # 前端静态页面目录 index index.html; } }这里最容易被忽略的就是client_max_body_size这一行,不加的话上传超过1MB的文件会直接报413 Request Entity Too Large。我第一次没配,传一个50MB的压缩包直接被拦,排查了半天才反应过来是Nginx默认限制。
配置改完后,用下面的命令重载Nginx:
nginx -s reload3.5 第五步:验证整套系统
全部服务启动后,按照下面的顺序做一次端到端验证:
- 浏览器打开
http://localhost,能看到登录页说明Nginx和前端正常; - 用初始化脚本里的测试账号登录,比如 admin / admin123;
- 上传一个50MB以上的文件,观察控制台输出的分片日志;
- 上传完成后,在文件列表点击下载,下载后用MD5工具比对源文件;
- 打开节点管理页,确认三个节点都在线。
如果以上步骤全部通过,那么你这套分布式存储平台的部署就彻底完成了。
4. 核心源码解读:分布式存储平台的三个关键机制
部署跑通只算拿到入场券,答辩时真正拉开差距的是你对核心源码的理解程度。我在下面展开三个必须彻底搞懂的机制。
4.1 一致性哈希路由:文件应该存到哪个节点
为什么不直接用nodeIndex = fileId % nodeCount这种简单取模?因为节点数量一旦变化,几乎所有文件的映射位置都会变,也就是说所有数据都要搬家。一致性哈希的价值在于:节点变化时只影响很少一部分数据。
核心实现逻辑很经典,用一个有序的环形结构来做:
// 一致性哈希环核心实现 public class ConsistentHashRouter<T> { private final TreeMap<Long, T> circle = new TreeMap<>(); private final int virtualNodeCount = 100; // 每个物理节点对应的虚拟节点数 public void addNode(T node) { for (int i = 0; i < virtualNodeCount; i++) { circle.put(hash(node.toString() + "#v" + i), node); } } public void removeNode(T node) { for (int i = 0; i < virtualNodeCount; i++) { circle.remove(hash(node.toString() + "#v" + i)); } } public T route(String fileId) { if (circle.isEmpty()) return null; Long key = hash(fileId); SortedMap<Long, T> tailMap = circle.tailMap(key); Long targetKey = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); return circle.get(targetKey); } private Long hash(String key) { // 实际项目中建议用MurmurHash等均衡性更好的算法 return (long) key.hashCode(); } }上面的代码里,route方法用tailMap找到第一个比文件哈希大的节点位置,如果找不到就回绕到环头。虚拟节点的作用是让节点分布更均匀,避免机器少时数据倾斜。
这段代码你不仅要会写,还要能讲出两点:为什么用TreeMap(有序+快速查找),以及为什么需要虚拟节点(解决物理节点哈希分布不均匀)。答辩时这一块非常加分。
4.2 分片上传与断点续传:大文件怎么处理
大文件在业务上不能一把梭直接传到管理节点再转存,那样管理节点内存会爆。实际方案是:前端先把文件切成512KB的块,逐个上传;管理节点收到每一块后,直接转发给目标存储节点落盘,同时记录块序号。这样整个系统任何时刻内存里只有一个分片的数据。
一个简化的入口控制器逻辑代码如下:
@RestController @RequestMapping("/file") public class FileController { @PostMapping("/upload") public Result uploadChunk(@RequestBody ChunkUploadRequest request) { // 1. 根据文件ID计算目标节点集合 List<StorageNode> targetNodes = router.routeNodes(request.getFileId()); // 2. 把分片数据并行写入多个副本节点 List<String> blockIds = storageClient.writeChunk( targetNodes, request.getChunkData(), request.getChunkIndex()); // 3. 写入成功后记录元数据 fileMetaService.saveChunkMeta(request, blockIds); return Result.success(); } }断点续传的实现也非常直接:前端在每次上传前,先调用一个查询接口,把已上传的分片序号列表拉回来,未上传的分片继续传。数据库里有一张表专门记录每个文件已经有哪些分片了,查询一次就能拿到。
4.3 副本写入与心跳检测:节点挂了怎么办
我在项目中配置的默认副本数是2,也就是每个分片至少要写入两个不同节点。这样即使某个节点挂掉,数据也能从副本恢复。副本选择的逻辑是:第一副本放在路由计算出的主节点,第二副本放在顺时针方向的下一个节点。
心跳机制我用的是存储节点主动上报模式。存储节点启动了一个TaskScheduler,每隔10秒调用一次管理节点的注册接口,上报节点状态和剩余容量。管理节点维护着一个“最近心跳时间”的Map,如果一个节点超过30秒没有心跳,就把它标记为离线。
@Component public class NodeHeartbeatTask { // 每10秒执行一次 @Scheduled(fixedRate = 10000) public void heartbeat() { String status = storageService.reportStatus(); adminClient.register(status); // 上报给管理节点 } }这个设计足够简单,答辩时也讲得清楚,“主动上报+超时离线”是分布式系统里最基础也最通用的状态管理方式。
5. 数据库设计:元数据与文件索引是系统的另一半
5.1 核心表结构详解
元数据是整个分布式存储平台的中枢神经系统,文件被存到了哪个节点、被切成几块、每块叫什么名字,全部要记录在数据库中。下面这几张表是我认为最核心、也最能在答辩时展示设计能力的地方:
-- 用户表 CREATE TABLE `tb_user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(128) NOT NULL, `role` TINYINT DEFAULT 1 COMMENT '0管理员 1普通用户', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 存储节点表 CREATE TABLE `tb_storage_node` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `node_id` VARCHAR(64) NOT NULL UNIQUE COMMENT '节点唯一标识', `ip` VARCHAR(32) NOT NULL, `port` INT NOT NULL, `status` TINYINT DEFAULT 1 COMMENT '0离线 1在线', `max_capacity` BIGINT COMMENT '节点总容量,字节', `used_capacity` BIGINT DEFAULT 0 COMMENT '已用容量', `last_heartbeat` DATETIME NULL COMMENT '最近心跳时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 文件元数据表 CREATE TABLE `tb_file_metadata` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `file_id` VARCHAR(64) NOT NULL COMMENT '业务文件唯一ID', `file_name` VARCHAR(255) NOT NULL, `file_size` BIGINT NOT NULL, `md5` VARCHAR(32) NOT NULL COMMENT '文件校验值', `user_id` BIGINT NOT NULL, `status` TINYINT DEFAULT 1 COMMENT '1正常 2回收站', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_file_id` (`file_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 文件分片表 CREATE TABLE `tb_file_block` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `file_id` VARCHAR(64) NOT NULL, `block_index` INT NOT NULL COMMENT '分片序号,从0开始', `block_size` BIGINT NOT NULL COMMENT '分片大小', `storage_node_id` VARCHAR(64) NOT NULL COMMENT '主副本所在节点', PRIMARY KEY (`id`), UNIQUE KEY `uk_file_block` (`file_id`, `block_index`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 副本表 CREATE TABLE `tb_file_copy` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `file_id` VARCHAR(64) NOT NULL, `block_index` INT NOT NULL, `storage_node_id` VARCHAR(64) NOT NULL COMMENT '副本所在节点', PRIMARY KEY (`id`), KEY `idx_node` (`storage_node_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套设计的核心是“文件元数据”和“分片位置信息”分开存储。tb_file_metadata只管业务层面的文件信息,比如文件名、大小、MD5;关于这个文件被拆成了哪几块、每块在哪个节点上,则分别由tb_file_block和tb_file_copy记录。
5.2 为什么元数据和文件数据要分开存
一个常见错误是,把节点信息直接冗余到文件表里。比如在文件表里加一个storage_node_id字段,表示文件存在哪个节点。表面上看省事,但一旦这个分片因为负载均衡被迁移到别的节点,或者副本失效,这些冗余字段就会变得不可信。
分开存的好处是:文件表描述“有什么”,分片表描述“在哪”,副本表描述“还有什么地方有”。每一层的信息变更互不影响,查询时通过一次多表关联或者两到三次简单查询就能拿到完整视图。
比如下载一个文件时,业务层的逻辑先查tb_file_metadata拿到MD5和文件大小,再查tb_file_block拿到所有分片和它们的主节点,最后查tb_file_copy决定从哪个副本拉取数据。这是一个清晰的三层索引链。
5.3 事务、MD5校验与一致性细节
元数据写入不能是“先写文件表,再写分片表,最后写副本表”这种一步一步来,一旦中间失败,库里就会出现“有文件无分片”的脏数据。我的做法是用Spring的@Transactional把一次文件上传的全部元数据写入包在一个事务里,任何一步失败整体回滚。
MD5校验则是下载完整性验证的核心。文件上传完成时计算一次MD5,存入tb_file_metadata;下载并合并所有分片后,再算一次MD5,两次比对一致才返回成功。这样就避免了“文件上传成功但数据损坏”的隐性 bug。
注意:实际操作中MySQL默认事务隔离级别是
REPEATABLE_READ,只要你在一次事务里完成所有写入,不会出现脏读问题。但存储节点落盘和数据库写入毕竟不是同一个原子操作,所以更严谨的做法是落盘成功后把块信息加入一个待确认队列,管理节点定时核对,这也是一个很好的答辩扩展点。
6. 部署与运行中的常见问题实录(这些问题我全踩过)
6.1 Nginx报413,上传大文件直接失败
这是部署阶段最让我抓狂的问题。现象是上传小文件一切正常,传一个几十MB的压缩包就立刻返回错误页面。排查链路很长:先怀疑后端接口写错,又怀疑前端超时,最后用curl -v -X POST直接测试接口,发现服务端压根没收到请求,才想到看Nginx的错误日志,里面明确写着client intended to send too large body。
问题根因是Nginx默认的client_max_body_size是1MB。解决办法就是在 server 块里加上client_max_body_size 100m;并重新加载配置,前面已经提到。这个坑非常典型,任何用Nginx做文件上传入口的项目都可能遇到。
6.2 MySQL连接报时区错误
启动管理节点时看到的报错大致是The server time zone value '�й���ʱ��' is unrecognized。原因是MySQL 8.0的时区信息和JDBC驱动默认处理方式不一致。在数据库连接URL里明确指定serverTimezone=Asia/Shanghai,或者在MySQL里执行set global time_zone = '+8:00'都能解决。如果这两步都做了还报错,检查一下URL里是不是被注释符号给挡住了。
6.3 Maven依赖下载慢或直接失败
首次用IDEA打开项目时,Maven会从中央仓库拉几百MB依赖,国内网络下经常超时。这不是项目问题,是仓库源问题。解决办法是在settings.xml里配置阿里云镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/central</url> </mirror>配置好之后重新导入项目,依赖下载速度会快很多。如果依赖下载到一半卡住,优先清理本地仓库缓存,不要把时间浪费在反复重试上。
6.4 存储节点启动正常,但管理端显示全部离线
存储节点明明显示Tomcat started on port(s): 8081,管理端节点管理页面却一片离线。我在排查时先确认了配置里的register-url是否可通,用curl http://127.0.0.1:8080/api/node/register手动请求一次,发现返回了401,才意识到管理节点接口做了登录鉴权,而存储节点的注册请求没有带Token。
处理方式有两种:一是把节点注册接口加入白名单,在拦截器配置里放行;二是让存储节点在启动时先调用登录接口获取Token,再携带Token完成注册。后者更安全,也更接近生产环境的做法。
6.5 端口冲突导致服务起不来
Windows下尤其常见,启动存储节点时提示Port 8082 was already in use。这通常是你之前跑过没关掉的实例,或者系统里有别的进程占用了端口。处理步骤:
# Windows netstat -ano | findstr 8082 taskkill /PID <进程号> /F # Linux lsof -i :8082 kill -9 <进程号>如果这个端口你有意保留给别的服务,就直接改application.yml里的server.port,换成8083、8084都行,项目配置很灵活,没有写死。
7. 答辩准备与项目扩展:做完之后怎么讲出彩
7.1 把项目讲清楚的顺序
答辩时不要上来就报代码行数和技术名词。我建议按“背景→架构→核心机制→验证结果”的顺序来讲:先讲单机存储有哪些痛点,再讲系统分为管理节点和存储节点这两大类服务,接着讲一致性哈希和副本策略是怎么实现的,最后现场演示上传一个文件然后停掉一个节点,证明系统仍能正常下载。这个演示是全场最有说服力的部分,比任何PPT都有效。
7.2 三个直接能用的扩展方向
如果时间充裕,或者你觉得这个题目的深度还可以再往上提一档,下面三个方向性价比很高:
- 加入Redis缓存:把最近访问的热点文件块缓存到Redis,下载时先走缓存,减少存储节点IO压力。这一块能表现出你对读写性能的思考。
- 引入消息队列做异步副本同步:管理节点收到分片后,把副本写入请求丢给消息队列,由消费者异步执行,减轻主链路延迟。用RabbitMQ或Kafka都可以,量级小用RabbitMQ更合理。
- 对接云存储作为冷备:将不常访问的文件迁移到对象存储服务,实现本地热数据+云上冷数据的分层存储。这个扩展点紧跟行业趋势,答辩老师通常都会感兴趣。
7.3 最后一点体会
做完这个题目之后,我的一个直观感受是:分布式存储平台的难点不在于某个算法有多高深,而在于你要同时保证“数据到底存到哪里了”这个问题的答案永远准确。文件落盘了,元数据没写进库;元数据写了,副本没同步成功;甚至是表结构设计得不合理,都会导致逻辑混乱。我在答辩时被问到最多的,反而不是某个具体函数,而是“如果存储节点在写入一半时宕机,你的系统会怎样”。这个问题只有真正动手做过、主动想过故障场景的人才能回答得清楚。
所以,多花点时间在设计和故障模拟上,比盲目加功能更有价值。这套东西做完,你收获的不仅是一个能跑的毕业设计,更是一套完整的分布式问题思考方法。后面即使换语言、换框架,核心逻辑都是相通的。