SpringBoot+RustFS 實(shí)現(xiàn)文件切片極速上傳的實(shí)例代碼
本文將手把手教你如何通過 SpringBoot 和 RustFS 構(gòu)建高性能文件切片上傳系統(tǒng),解決大文件傳輸?shù)耐袋c(diǎn),實(shí)現(xiàn)秒傳、斷點(diǎn)續(xù)傳和分片上傳等高級功能。
一、為什么選擇 RustFS + SpringBoot?
在傳統(tǒng)文件上傳方案中,大文件傳輸面臨諸多挑戰(zhàn):網(wǎng)絡(luò)傳輸不穩(wěn)定、服務(wù)器內(nèi)存溢出、上傳失敗需重新傳輸等。而 ?RustFS? 作為一款基于 Rust 語言開發(fā)的高性能分布式對象存儲系統(tǒng),具有以下突出優(yōu)勢:
- ?高性能?:充分利用 Rust 的內(nèi)存安全和高并發(fā)特性,響應(yīng)速度極快
- ?分布式架構(gòu)?:可擴(kuò)展且具備容錯能力,適用于海量數(shù)據(jù)存儲
- ?AWS S3 兼容性?:支持標(biāo)準(zhǔn) S3 API,可與現(xiàn)有生態(tài)無縫集成
- ?可視化管理?:內(nèi)置功能豐富的 Web 控制臺,管理更方便
- ?開源友好?:采用 Apache 2.0 協(xié)議,鼓勵社區(qū)貢獻(xiàn)
結(jié)合 SpringBoot 的快速開發(fā)特性,我們可以輕松構(gòu)建企業(yè)級文件上傳解決方案。
二、環(huán)境準(zhǔn)備與部署
2.1 安裝 RustFS
使用 Docker 快速部署 RustFS:
# docker-compose.yml
version: '3.8'
services:
rustfs:
image: registry.cn-shanghai.aliyuncs.com/study-03/rustfs:latest
container_name: rustfs
ports:
- "9000:9000" # 管理控制臺
- "9090:9090" # API服務(wù)端口
volumes:
- ./data:/data
environment:
- RUSTFS_ROOT_USER=admin
- RUSTFS_ROOT_PASSWORD=admin123
restart: unless-stopped運(yùn)行 docker-compose up -d即可啟動服務(wù)。訪問 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:9090 access-key: admin secret-key: admin123 bucket-name: mybucket
三、核心代碼實(shí)現(xiàn)
3.1 配置 RustFS 客戶端
@Configuration
@ConfigurationProperties(prefix = "rustfs")
public class RustFSConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
@Bean
public MinioClient rustFSClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}3.2 文件切片上傳服務(wù)
@Service
@Slf4j
public class FileUploadService {
@Autowired
private MinioClient rustFSClient;
@Value("${rustfs.bucket-name}")
private String bucketName;
/**
* 初始化分片上傳
*/
public String initUpload(String fileName, String fileMd5) {
String uploadId = UUID.randomUUID().toString();
// 檢查文件是否已存在(秒傳實(shí)現(xiàn))
if (checkFileExists(fileMd5)) {
throw new RuntimeException("文件已存在,可直接秒傳");
}
// 存儲上傳記錄到Redis或數(shù)據(jù)庫
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;
// 上傳分片到RustFS
rustFSClient.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 {
// 創(chuàng)建臨時文件
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) {
// 查詢數(shù)據(jù)庫或Redis檢查文件是否已存在
return redisTemplate.hasKey("file:" + fileMd5);
}
}3.3 控制器實(shí)現(xiàn)
@RestController
@RequestMapping("/api/upload")
@Slf4j
public class FileUploadController {
@Autowired
private 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("文件合并成功");
}
/**
* 獲取已上傳分片列表(斷點(diǎn)續(xù)傳)
*/
@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);
}
}四、前端實(shí)現(xiàn)關(guān)鍵代碼
4.1 文件切片處理
class FileUploader {
constructor() {
this.chunkSize = 5 * 1024 * 1024; // 5MB分片大小
this.concurrentLimit = 3; // 并發(fā)上傳數(shù)
}
// 計算文件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) {
// 計算文件MD5
const 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;
}
// 獲取已上傳分片(斷點(diǎn)續(xù)傳)
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);
// 控制并發(fā)數(shù)
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}`);
}
}
}五、高級功能與優(yōu)化
5.1 斷點(diǎn)續(xù)傳實(shí)現(xiàn)
通過記錄已上傳的分片信息,實(shí)現(xiàn)上傳中斷后從中斷處繼續(xù)上傳:
@Service
public class UploadProgressService {
@Autowired
private 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 分片驗(yàn)證與安全
確保分片傳輸?shù)耐暾院桶踩裕?/p>
/**
* 分片驗(yàn)證服務(wù)
*/
@Service
public class ChunkValidationService {
/**
* 驗(yàn)證分片哈希
*/
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;
}
}
/**
* 驗(yàn)證分片順序
*/
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();
}
}六、部署與性能優(yōu)化
6.1 系統(tǒng)級優(yōu)化建議
?分片大小選擇?
- 內(nèi)網(wǎng)環(huán)境:10MB-20MB
- 移動網(wǎng)絡(luò):1MB-5MB
- 廣域網(wǎng):500KB-1MB
?并發(fā)控制?
# 應(yīng)用配置
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB?定時清理策略?
@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 監(jiān)控與告警
集成 Prometheus 監(jiān)控上傳性能:
# 監(jiān)控指標(biāo)配置
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}七、總結(jié)
通過 SpringBoot 和 RustFS 的組合,我們實(shí)現(xiàn)了一個高性能的文件切片上傳系統(tǒng),具備以下優(yōu)勢:
- ?高性能?:利用 RustFS 的高并發(fā)特性和分片并行上傳,大幅提升傳輸速度
- ?可靠性?:斷點(diǎn)續(xù)傳機(jī)制確保上傳中斷后從中斷處繼續(xù),避免重復(fù)勞動
- ?智能優(yōu)化?:秒傳功能避免重復(fù)文件上傳,節(jié)省帶寬和存儲空間
- ?易于擴(kuò)展?:分布式架構(gòu)支持水平擴(kuò)展,適應(yīng)不同規(guī)模的應(yīng)用場景
這種方案特別適用于:
- 視頻平臺的大文件上傳
- 企業(yè)級文檔管理系統(tǒng)
- 云存儲和備份服務(wù)
- AI 訓(xùn)練數(shù)據(jù)集上傳
到此這篇關(guān)于SpringBoot+RustFS 實(shí)現(xiàn)文件切片極速上傳的實(shí)例代碼的文章就介紹到這了,更多相關(guān)SpringBoot RustFS 文件切片上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java基于Socket實(shí)現(xiàn)HTTP下載客戶端
這篇文章主要介紹了Java基于Socket實(shí)現(xiàn)HTTP下載客戶端的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-01-01
KotlinScript構(gòu)建SpringBootStarter保姆級教程
這篇文章主要為大家介紹了KotlinScript構(gòu)建SpringBootStarter的保姆級教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
idea中使用Inputstream流導(dǎo)致中文亂碼解決方法
很多朋友遇到一個措手不及的問題當(dāng)idea中使用Inputstream流導(dǎo)致中文亂碼及Java FileInputStream讀中文亂碼問題,針對這兩個問題很多朋友不知道該如何解決,下面小編把解決方案分享給大家供大家參考2021-05-05
JAVA 聚焦 OutOfMemoryError 異常問題記錄
在 Java 開發(fā)中,內(nèi)存溢出異常是影響程序穩(wěn)定性的關(guān)鍵問題,了解其原理和應(yīng)對方法,對開發(fā)者至關(guān)重要,這篇文章主要介紹了JAVA聚焦 OutOfMemoryError 異常,需要的朋友可以參考下2025-04-04
Java AOP實(shí)現(xiàn)自定義滑動窗口限流器方法詳解
這篇文章主要介紹了Java AOP實(shí)現(xiàn)自定義滑動窗口限流器方法,其中滑動窗口算法彌補(bǔ)了計數(shù)器算法的不足,滑動窗口算法把間隔時間劃分成更小的粒度,當(dāng)更小粒度的時間間隔過去后,把過去的間隔請求數(shù)減掉,再補(bǔ)充一個空的時間間隔,需要的朋友可以參考下2022-07-07
Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法
下面小編就為大家分享一篇Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助2017-11-11

