本文将手把手教你如何通过 SpringBoot 和 RustFS 构建高性能文件切片上传系统,解决大文件传输的痛点,实现秒传、断点续传和分片上传等高级功能。

目录

一、为什么选择 RustFS + SpringBoot?

二、环境准备与部署

2.1 安装 RustFS

2.2 SpringBoot 项目配置

三、核心代码实现

3.1 配置 RustFS 客户端

3.2 文件切片上传服务

3.3 控制器实现

四、前端实现关键代码

4.1 文件切片处理

五、高级功能与优化

5.1 断点续传实现

5.2 分片验证与安全

六、部署与性能优化

6.1 系统级优化建议

6.2 监控与告警

七、总结


一、为什么选择 RustFS + SpringBoot?

在传统文件上传方案中,大文件传输面临诸多挑战:网络传输不稳定、服务器内存溢出、上传失败需重新传输等。而 ​RustFS​ 作为一款基于 Rust 语言开发的高性能分布式对象存储系统,具有以下突出优势:

  • 高性能​:充分利用 Rust 的内存安全和高并发特性,响应速度极快

  • 分布式架构​:可扩展且具备容错能力,适用于海量数据存储

  • AWS S3 兼容性​:支持标准 S3 API,可与现有生态无缝集成

  • 可视化管理​:内置功能丰富的 Web 控制台,管理更方便

  • 开源友好​:采用 Apache 2.0 协议,鼓励社区贡献

结合 SpringBoot 的快速开发特性,我们可以轻松构建企业级文件上传解决方案。

二、环境准备与部署

2.1 安装 RustFS

使用 Docker 快速部署 RustFS:

# docker-compose.yml
version: '3.8'
services:rustfs:image: registry.cn-shanghai.aliyuncs.com/study-03/rustfs:latestcontainer_name: rustfsports:- "9000:9000"  # 管理控制台- "9090:9090"  # API服务端口volumes:- ./data:/dataenvironment:- RUSTFS_ROOT_USER=admin- RUSTFS_ROOT_PASSWORD=admin123restart: unless-stopped

运行 docker-compose up -d即可启动服务。访问 http://localhost:9000使用 admin/admin123 登录管理控制台。

2.2 SpringBoot 项目配置

pom.xml中添加必要依赖:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.2</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version></dependency>
</dependencies>

配置 application.yml:

rustfs:endpoint: http://localhost:9090access-key: adminsecret-key: admin123bucket-name: mybucket

三、核心代码实现

3.1 配置 RustFS 客户端

@Configuration
@ConfigurationProperties(prefix = "rustfs")
public class RustFSConfig {private String endpoint;private String accessKey;private String secretKey;private String bucketName;@Beanpublic MinioClient rustFSClient() {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();}
}

3.2 文件切片上传服务

@Service
@Slf4j
public class FileUploadService {@Autowiredprivate MinioClient rustFSClient;@Value("${rustfs.bucket-name}")private String bucketName;/*** 初始化分片上传*/public String initUpload(String fileName, String fileMd5) {String uploadId = UUID.randomUUID().toString();// 检查文件是否已存在(秒传实现)if (checkFileExists(fileMd5)) {throw new RuntimeException("文件已存在,可直接秒传");}// 存储上传记录到Redis或数据库redisTemplate.opsForValue().set("upload:" + fileMd5, uploadId);return uploadId;}/*** 上传文件分片*/public void uploadChunk(MultipartFile chunk, String fileMd5, int chunkIndex, int totalChunks) {try {// 生成分片唯一名称String chunkName = fileMd5 + "_chunk_" + chunkIndex;// 上传分片到RustFSrustFSClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(chunkName).stream(chunk.getInputStream(), chunk.getSize(), -1).build());// 记录已上传分片redisTemplate.opsForSet().add("chunks:" + fileMd5, chunkIndex);} catch (Exception e) {log.error("分片上传失败", e);throw new RuntimeException("分片上传失败");}}/*** 合并文件分片*/public void mergeChunks(String fileMd5, String fileName, int totalChunks) {try {// 创建临时文件Path tempFile = Files.createTempFile("merge_", ".tmp");try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {// 按顺序下载并合并所有分片for (int i = 0; i < totalChunks; i++) {String chunkName = fileMd5 + "_chunk_" + i;try (InputStream is = rustFSClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(chunkName).build())) {IOUtils.copy(is, fos);}// 删除已合并的分片rustFSClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(chunkName).build());}}// 上传最终文件rustFSClient.uploadObject(UploadObjectArgs.builder().bucket(bucketName).object(fileName).filename(tempFile.toString()).build());// 清理临时文件Files.deleteIfExists(tempFile);// 更新文件记录saveFileRecord(fileMd5, fileName);} catch (Exception e) {log.error("分片合并失败", e);throw new RuntimeException("分片合并失败");}}/*** 检查文件是否存在(秒传功能)*/private boolean checkFileExists(String fileMd5) {// 查询数据库或Redis检查文件是否已存在return redisTemplate.hasKey("file:" + fileMd5);}
}

3.3 控制器实现

@RestController
@RequestMapping("/api/upload")
@Slf4j
public class FileUploadController {@Autowiredprivate FileUploadService fileUploadService;/*** 初始化上传*/@PostMapping("/init")public ResponseEntity<?> initUpload(@RequestParam String fileName,@RequestParam String fileMd5) {try {String uploadId = fileUploadService.initUpload(fileName, fileMd5);return ResponseEntity.ok(Map.of("uploadId", uploadId));} catch (RuntimeException e) {return ResponseEntity.ok(Map.of("exists", true)); // 文件已存在}}/*** 上传分片*/@PostMapping("/chunk")public ResponseEntity<String> uploadChunk(@RequestParam MultipartFile chunk,@RequestParam String fileMd5,@RequestParam int chunkIndex,@RequestParam int totalChunks) {fileUploadService.uploadChunk(chunk, fileMd5, chunkIndex, totalChunks);return ResponseEntity.ok("分片上传成功");}/*** 合并分片*/@PostMapping("/merge")public ResponseEntity<String> mergeChunks(@RequestParam String fileMd5,@RequestParam String fileName,@RequestParam int totalChunks) {fileUploadService.mergeChunks(fileMd5, fileName, totalChunks);return ResponseEntity.ok("文件合并成功");}/*** 获取已上传分片列表(断点续传)*/@GetMapping("/chunks/{fileMd5}")public ResponseEntity<List<Integer>> getUploadedChunks(@PathVariable String fileMd5) {Set<Object> uploaded = redisTemplate.opsForSet().members("chunks:" + fileMd5);List<Integer> chunks = uploaded.stream().map(obj -> Integer.parseInt(obj.toString())).collect(Collectors.toList());return ResponseEntity.ok(chunks);}
}

四、前端实现关键代码

4.1 文件切片处理

class FileUploader {constructor() {this.chunkSize = 5 * 1024 * 1024; // 5MB分片大小this.concurrentLimit = 3; // 并发上传数}// 计算文件MD5(秒传功能)async calculateFileMD5(file) {return new Promise((resolve) => {const reader = new FileReader();const spark = new SparkMD5.ArrayBuffer();reader.onload = e => {spark.append(e.target.result);resolve(spark.end());};reader.readAsArrayBuffer(file);});}// 切片上传async uploadFile(file) {// 计算文件MD5const fileMd5 = await this.calculateFileMD5(file);// 初始化上传const initResponse = await fetch('/api/upload/init', {method: 'POST',body: JSON.stringify({fileName: file.name,fileMd5: fileMd5}),headers: {'Content-Type': 'application/json'}});const initResult = await initResponse.json();// 如果文件已存在,直接返回if (initResult.exists) {alert('文件已存在,秒传成功!');return;}// 获取已上传分片(断点续传)const uploadedChunks = await this.getUploadedChunks(fileMd5);// 计算分片信息const totalChunks = Math.ceil(file.size / this.chunkSize);const uploadPromises = [];for (let i = 0; i < totalChunks; i++) {// 跳过已上传的分片if (uploadedChunks.includes(i)) {continue;}const start = i * this.chunkSize;const end = Math.min(file.size, start + this.chunkSize);const chunk = file.slice(start, end);// 控制并发数if (uploadPromises.length >= this.concurrentLimit) {await Promise.race(uploadPromises);}const uploadPromise = this.uploadChunk(chunk, fileMd5, i, totalChunks).finally(() => {const index = uploadPromises.indexOf(uploadPromise);if (index > -1) {uploadPromises.splice(index, 1);}});uploadPromises.push(uploadPromise);}// 等待所有分片上传完成await Promise.all(uploadPromises);// 合并分片await this.mergeChunks(fileMd5, file.name, totalChunks);}// 上传单个分片async uploadChunk(chunk, fileMd5, chunkIndex, totalChunks) {const formData = new FormData();formData.append('chunk', chunk);formData.append('fileMd5', fileMd5);formData.append('chunkIndex', chunkIndex);formData.append('totalChunks', totalChunks);const response = await fetch('/api/upload/chunk', {method: 'POST',body: formData});if (!response.ok) {throw new Error(`分片上传失败: ${response.statusText}`);}}
}

五、高级功能与优化

5.1 断点续传实现

通过记录已上传的分片信息,实现上传中断后从中断处继续上传:

@Service
public class UploadProgressService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 获取已上传分片列表*/public List<Integer> getUploadedChunks(String fileMd5) {Set<Object> uploaded = redisTemplate.opsForSet().members("chunks:" + fileMd5);return uploaded.stream().map(obj -> Integer.parseInt(obj.toString())).collect(Collectors.toList());}/*** 清理上传记录*/public void clearUploadRecord(String fileMd5) {redisTemplate.delete("chunks:" + fileMd5);redisTemplate.delete("upload:" + fileMd5);}
}

5.2 分片验证与安全

确保分片传输的完整性和安全性:

/*** 分片验证服务*/
@Service
public class ChunkValidationService {/*** 验证分片哈希*/public boolean validateChunkHash(MultipartFile chunk, String expectedHash) {try {String actualHash = HmacUtils.hmacSha256Hex("secret-key", chunk.getBytes());return actualHash.equals(expectedHash);} catch (IOException e) {return false;}}/*** 验证分片顺序*/public boolean validateChunkOrder(String fileMd5, int chunkIndex) {// 获取已上传分片Set<Object> uploaded = redisTemplate.opsForSet().members("chunks:" + fileMd5);List<Integer> chunks = uploaded.stream().map(obj -> Integer.parseInt(obj.toString())).sorted().collect(Collectors.toList());// 检查分片是否按顺序上传return chunks.isEmpty() || chunkIndex == chunks.size();}
}

六、部署与性能优化

6.1 系统级优化建议

  1. 分片大小选择

    • 内网环境:10MB-20MB

    • 移动网络:1MB-5MB

    • 广域网:500KB-1MB

  2. 并发控制

    # 应用配置
    spring:servlet:multipart:max-file-size: 10MBmax-request-size: 100MB
  3. 定时清理策略

    @Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每日清理
    public void cleanTempFiles() {// 删除超过24小时的临时分片redisTemplate.keys("chunks:*").forEach(key -> {if (redisTemplate.getExpire(key) < 0) {redisTemplate.delete(key);}});
    }

6.2 监控与告警

集成 Prometheus 监控上传性能:

# 监控指标配置
management:endpoints:web:exposure:include: health,metrics,prometheusmetrics:tags:application: ${spring.application.name}

七、总结

通过 SpringBoot 和 RustFS 的组合,我们实现了一个高性能的文件切片上传系统,具备以下优势:

  1. 高性能​:利用 RustFS 的高并发特性和分片并行上传,大幅提升传输速度

  2. 可靠性​:断点续传机制确保上传中断后从中断处继续,避免重复劳动

  3. 智能优化​:秒传功能避免重复文件上传,节省带宽和存储空间

  4. 易于扩展​:分布式架构支持水平扩展,适应不同规模的应用场景

这种方案特别适用于:

  • 视频平台的大文件上传

  • 企业级文档管理系统

  • 云存储和备份服务

  • AI 训练数据集上传

希望本文能帮助你在实际项目中实现高效可靠的文件上传功能。如果有任何问题或建议,欢迎在评论区交流讨论!


以下是深入学习 RustFS 的推荐资源:RustFS

官方文档: RustFS 官方文档- 提供架构、安装指南和 API 参考。

GitHub 仓库: GitHub 仓库 - 获取源代码、提交问题或贡献代码。

社区支持: GitHub Discussions- 与开发者交流经验和解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/96315.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/96315.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/96315.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Word和WPS文字中便捷切换英文段落大小写

在Word和WPS文字中编辑英文段落时&#xff0c;有时候英文字母的大小写不规范&#xff0c;或者需要把某一段全部改为大写字母怎么办&#xff1f;使用ShiftF3组合键即可快速在三种模式中切换&#xff1a;全部大写、全部小写、首字母大写——其中首字母大写的Word是每一句话的第一…

成都金牛区哪里租好办公室?国际数字影像产业园享税收优惠

在成都金牛区租赁优质办公室&#xff0c;国际数字影像产业园凭借其享有的税收优惠政策&#xff0c;成为了许多企业的首选之地。税收优惠对于租赁办公室的企业来说&#xff0c;是一笔不小的成本节省。国际数字影像产业园针对入驻企业提供的税收优惠政策&#xff0c;能在企业运营…

CSS `:is()` `:where()` 实战指南:简化选择器,提升可维护性

&#x1f3af; CSS :is() & :where() 实战指南&#xff1a;简化选择器&#xff0c;提升可维护性你是否在项目中写过一大串重复的选择器&#xff1f;比如&#xff1a; h1, h2, h3, h4, h5, h6 { margin-bottom: 1rem; }这样的代码既冗长又难维护。 现在 CSS 提供了 :is() 和…

Linux I/O 访问架构深入分析

Linux I/O 访问架构深入分析 目录 概述I/O 架构层次核心数据结构I/O 处理流程VFS 虚拟文件系统块设备I/O字符设备I/O内存映射I/O异步I/O机制I/O调度器调试工具与方法性能优化策略 概述 Linux I/O 系统是一个多层次、高度抽象的架构&#xff0c;旨在为应用程序提供统一的文件访问…

Linux:6_基础IO

基础IO 一.理解"文件" 文件分类 1.内存级(被打开)文件 2.磁盘级文件 1. 狭义理解 文件在磁盘里磁盘是永久性存储介质&#xff0c;因此文件在磁盘上的存储是永久性的磁盘是外设 (即是输出设备也是输入设备)磁盘上的文件本质是对文件的所有操作&#xff0c;都是对外…

Coze源码分析-资源库-删除插件-前端源码-核心逻辑

删除插件逻辑 1. 删除操作入口组件 删除插件操作主要通过 usePluginConfig hook 中的 renderActions 方法实现&#xff0c;该方法返回 TableAction 组件来处理表格行的操作。 文件位置&#xff1a;frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/u…

第一代:嵌入式本地状态(Flink 1.x)

最初的架构将状态以 JVM Heap 对象的形式存储在 TaskManager 的内存中。对于小规模数据集&#xff0c;这种方式效果良好&#xff0c;但随着状态大小的增长超出内存&#xff0c;将所有状态保存在内存中变得成本高昂且不稳定。 为了解决状态规模增长的问题&#xff0c;引入了一种…

跨境金融数据对接实践:印度NSE/BSE股票行情API集成指南

跨境金融数据对接实践&#xff1a;印度NSE/BSE股票行情API集成指南 关键词&#xff1a;印度股票数据对接 NSE实时行情 BSE证券接口 金融API开发 Python请求示例一、印度股市数据源技术解析&#xff08;核心价值&#xff09; 印度两大交易所数据获取难点&#xff1a; 时区差异&a…

AFSim2.9.0学习笔记 —— 1、AFSim及完整工具介绍(文末附:完整afsim2.9.0源码、编译好的完整工具包、中文教材等)

&#x1f514; AFSim2.9.0 相关技术、疑难杂症文章合集&#xff08;掌握后可自封大侠 ⓿_⓿&#xff09;&#xff08;记得收藏&#xff0c;持续更新中…&#xff09; AFSim介绍 AFSim&#xff08;Advanced Framework for Simulation Integration & Modeling【高级仿真集成与…

ArcGIS学习-18 实战-降雨量空间分布插值分析

设置环境加载要素投影查看要素&#xff0c;发现均不是投影数据&#xff0c;但都是地理坐标都是WGS1984使用工具进行批量投影然后新建空地图&#xff0c;重新加载确认图层的投影与栅格数据一致插值样条法得到反距离权重法插值得到克里金法插值得到

HarmonyOS应用开发:深入理解声明式UI与弹窗交互的最佳实践

HarmonyOS应用开发&#xff1a;深入理解声明式UI与弹窗交互的最佳实践 引言 随着HarmonyOS 4.0的发布及后续版本的演进&#xff0c;华为的分布式操作系统已经进入了全新的发展阶段。基于API 12及以上的开发环境为开发者提供了更强大、更高效的开发工具和框架。在HarmonyOS应用…

探索Java并发编程--从基础到高级实践技巧

Thread&#xff08;线程&#xff09;线程 程序执行的最小单位&#xff08;一个进程至少有一个线程&#xff09;。线程内有自己的执行栈、程序计数器&#xff08;PC&#xff09;&#xff0c;但与同进程内其他线程共享堆内存与进程资源 在java中&#xff0c;线程由java.lang.Thr…

Go语言实战案例-开发一个Markdown转HTML工具

这个小工具可以把 .md 文件转换为 .html 文件&#xff0c;非常适合写笔记、博客或者快速预览 Markdown 内容。&#x1f4cc; 案例目标• 读取一个 Markdown 文件• 使用开源库将 Markdown 转换为 HTML• 将 HTML 输出到新文件中&#x1f4e6; 所需库我们用 goldmark 这个 Markd…

基于51单片机的太阳能锂电池充电路灯

基于51单片机的太阳能锂电池充电路灯系统设计 1 系统功能介绍 本设计以 STC89C52单片机 为核心&#xff0c;构建了一个能够利用太阳能为锂电池充电并智能控制LED路灯的系统。系统结合了 光照检测电路、LED灯电路、按键检测电路、太阳能充电电路 等模块&#xff0c;实现了节能、…

PAT 1178 File Path

这一题的大意是给出了一个windows的文件夹目录&#xff0c;让我们按照所属的目录关系&#xff0c;来找相应的目录是否存在&#xff0c;如果存在&#xff0c;就输出找到该文件的路径&#xff0c;如果不存在输出error 我的思路是用合适的树形结构保存下来目录的所属关系&#xff…

云原生部署_k8s入门

K8S官网文档&#xff1a;&#xfeff;https://kubernetes.io/zh/docs/home/Kubernetes是什么Kubernetes 是用于自动部署、扩缩和管理容器化应用程序的开源系统。 Kubernetes 源自 &#xff0c;Google 15 年生产环境的运维经验同时凝聚了社区的最佳创意和实践。简称K8s.Kubernet…

实战项目-----Python+OpenCV 实现对视频的椒盐噪声注入与实时平滑还原”

实战项目实现以下功能&#xff1a;功能 1&#xff1a;为视频每一帧添加椒盐噪声作用&#xff1a;模拟真实环境中图像传输或采集时可能出现的噪声。实现方式&#xff1a;读取视频的每一帧。随机选择 10000 个像素点&#xff0c;将其设置为黑色&#xff08;0&#xff09;或白色&a…

Day42 PHP(mysql注入、跨库读取)

一、sql注入基本原理&#xff1a;没有对用户输入的数据进行限制&#xff0c;导致数据库语句可以做什么&#xff0c;用户就可以做什么。取决于不同数据库的不同查询语言&#xff0c;所以为什么有mysql注入/orcale注入等等。步骤&#xff1a;【access】表名&#xff08;字典爆破来…

机器人控制器开发(部署——软件打包备份更新)

文章总览 为什么做备份更新 为机器人控制器设计一套打包备份更新机制&#xff0c;为控制器的批量生产和产品与项目落地做准备。 当某个模块出现bug需要升级时&#xff0c;用户可以快速获取正确的bak包并导入到控制器中重启生效。 如果没有做好软件的备份更新机制&#xff0c…

LaTeX TeX Live 安装与 CTAN 国内镜像配置(Windows / macOS / Linux 全流程)

这是一份面向国内环境的 LaTeX 从零到可编译 指南&#xff1a;覆盖 TeX Live / MacTeX 安装、PATH 配置、CTAN 国内镜像&#xff08;清华/北外/上交/中科大等&#xff09;一键切换与回滚、常见坑位&#xff08;权限、镜像路径、版本切换&#xff09;、以及 XeLaTeX/latexmk 的实…