1. 大文件分块上传与断点续传技术解析
在企业级文件传输系统中,大文件上传一直是技术难点。传统单次上传方式存在网络波动敏感、内存占用高、失败后需重传等问题。我们团队通过自研分块上传方案,实现了稳定可靠的TB级文件传输能力。
核心原理:将大文件切割为5MB大小的分片,每个分片独立上传并记录状态。即使中途断网,也能从最后一个成功分片继续上传,避免重复传输已完成部分。
2. 技术架构设计要点
2.1 整体架构分层
系统采用前后端分离设计,主要分为四个逻辑层:
- 前端展示层:Vue2/React实现用户界面,包含文件选择器、进度展示等组件
- 分片调度层:控制分片上传顺序、失败重试机制
- 传输引擎层:处理分片合并、断点续传逻辑
- 存储服务层:对接阿里云OSS或本地文件系统
2.2 关键技术选型考量
选择技术栈时我们重点考虑以下因素:
- 兼容性:需支持IE8等老旧浏览器(央企常见要求)
- 安全性:满足国密SM4加密标准
- 稳定性:分片上传失败后能自动恢复
- 性能:支持10万级并发分片传输
最终采用的方案组合:
- 前端:Resumable.js增强版(IE8兼容)
- 加密:BouncyCastle国密SM4实现
- 存储:MySQL记录分片元数据
3. 核心实现代码详解
3.1 后端分片接收接口
java复制@RestController
@RequestMapping("/api/file")
public class FileTransferController {
@PostMapping("/upload")
public ResponseEntity uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("identifier") String identifier) {
try {
// 创建分片存储目录
Path tempDir = Paths.get("/tmp/upload/" + identifier);
Files.createDirectories(tempDir);
// 存储分片到临时文件
Files.write(tempDir.resolve("chunk-" + chunkNumber),
file.getBytes(),
StandardOpenOption.CREATE);
// 更新数据库记录
chunkService.updateChunkStatus(identifier, chunkNumber);
return ResponseEntity.ok("Chunk uploaded");
} catch (Exception e) {
return ResponseEntity.status(500)
.body("Upload failed: " + e.getMessage());
}
}
}
关键点说明:
- 每个分片以独立文件形式存储
- 使用identifier作为文件唯一标识
- 数据库实时更新分片上传状态
3.2 分片合并逻辑
java复制public void mergeChunks(String identifier, String fileName) throws IOException {
// 获取所有分片文件
Path tempDir = Paths.get("/tmp/upload/" + identifier);
List<Path> chunks = Files.list(tempDir)
.sorted(Comparator.comparing(p -> {
String name = p.getFileName().toString();
return Integer.parseInt(name.split("-")[1]);
}))
.collect(Collectors.toList());
// 创建目标文件
Path output = Paths.get("/data/uploads/" + fileName);
try (OutputStream os = Files.newOutputStream(output,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND)) {
// 按顺序合并分片
for (Path chunk : chunks) {
Files.copy(chunk, os);
Files.delete(chunk); // 合并后删除分片
}
}
// 更新数据库状态
taskService.markTaskComplete(identifier);
}
4. 前端实现关键代码
4.1 分片上传逻辑
javascript复制// FileUploader.vue
methods: {
uploadChunk(file, chunkNumber) {
const start = chunkNumber * this.chunkSize;
const end = Math.min(file.size, start + this.chunkSize);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkNumber', chunkNumber);
formData.append('identifier', this.fileId);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/file/upload', true);
xhr.onload = () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error('Upload failed'));
}
};
xhr.send(formData);
});
}
}
4.2 断点续传实现
javascript复制async resumeUpload(file) {
// 检查已上传分片
const res = await axios.get(`/api/file/progress?identifier=${this.fileId}`);
const uploadedChunks = res.data.chunks;
// 续传未完成分片
for (let i = 0; i < this.totalChunks; i++) {
if (!uploadedChunks.includes(i)) {
await this.uploadChunk(file, i);
}
}
}
5. 数据库设计优化
5.1 分片记录表结构
sql复制CREATE TABLE `file_chunks` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`task_id` VARCHAR(64) NOT NULL,
`chunk_number` INT NOT NULL,
`status` TINYINT DEFAULT 0 COMMENT '0-未上传 1-已上传',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_task` (`task_id`)
) ENGINE=InnoDB;
5.2 断点续传查询优化
java复制@Repository
public interface FileChunkRepository extends JpaRepository<FileChunk, Long> {
@Query("SELECT c.chunkNumber FROM FileChunk c " +
"WHERE c.taskId = :taskId AND c.status = 1")
List<Integer> findUploadedChunks(@Param("taskId") String taskId);
@Modifying
@Query("UPDATE FileChunk c SET c.status = 1 " +
"WHERE c.taskId = :taskId AND c.chunkNumber = :chunkNumber")
void markChunkUploaded(@Param("taskId") String taskId,
@Param("chunkNumber") int chunkNumber);
}
6. 性能优化实践
6.1 分片大小选择
经过测试不同分片大小的传输效率:
| 分片大小 | 上传耗时(100MB) | 失败恢复效率 |
|---|---|---|
| 1MB | 28s | 高 |
| 5MB | 22s | 中高 |
| 10MB | 20s | 中 |
| 20MB | 18s | 低 |
最终选择5MB作为平衡点,既保证传输效率,又便于断点续传。
6.2 并发上传控制
前端采用3个并行上传队列:
javascript复制// 并发上传控制
const MAX_CONCURRENT = 3;
let activeUploads = 0;
async uploadAllChunks() {
const chunksToUpload = // 获取待上传分片
while (chunksToUpload.length > 0) {
if (activeUploads < MAX_CONCURRENT) {
activeUploads++;
const chunk = chunksToUpload.shift();
this.uploadChunk(chunk)
.finally(() => activeUploads--);
} else {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
7. 安全加固措施
7.1 文件校验机制
合并分片时进行完整性检查:
java复制public void validateFile(Path file, String expectedHash) throws IOException {
try (InputStream is = Files.newInputStream(file)) {
String actualHash = DigestUtils.md5Hex(is);
if (!expectedHash.equals(actualHash)) {
throw new IOException("File verification failed");
}
}
}
7.2 临时文件清理
配置定时任务清理过期临时文件:
java复制@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void cleanTempFiles() {
Path tempDir = Paths.get("/tmp/upload");
Files.walk(tempDir)
.filter(p -> Files.isDirectory(p))
.filter(p -> {
long modified = Files.getLastModifiedTime(p).toMillis();
return System.currentTimeMillis() - modified > 86400000; // 24小时
})
.forEach(p -> {
try {
FileUtils.deleteDirectory(p.toFile());
} catch (IOException e) {
log.error("Clean temp dir failed", e);
}
});
}
8. 异常处理方案
8.1 分片上传失败重试
前端实现自动重试逻辑:
javascript复制async uploadWithRetry(chunk, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await this.uploadChunk(chunk);
return;
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
8.2 服务端错误处理
统一异常处理增强:
java复制@ControllerAdvice
public class FileUploadExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity handleSizeExceeded() {
return ResponseEntity.status(413)
.body("File size exceeds limit");
}
@ExceptionHandler(IOException.class)
public ResponseEntity handleIOError(IOException e) {
return ResponseEntity.status(500)
.body("Storage error: " + e.getMessage());
}
}
9. 实际部署经验
9.1 Nginx配置优化
调整上传相关参数:
nginx复制client_max_body_size 10240m; # 允许10GB文件上传
client_body_temp_path /data/nginx/temp; # 使用高速存储
proxy_read_timeout 600s; # 长超时设置
9.2 文件存储规划
推荐目录结构:
code复制/data/uploads/
├── 2023/
│ ├── 08/
│ │ ├── 01/
│ │ │ ├── guid1/
│ │ │ │ └── filename.ext
│ │ │ └── guid2/
├── temp/ # 分片临时目录
10. 测试验证方案
10.1 自动化测试用例
关键测试场景覆盖:
java复制@Test
public void testChunkUploadAndMerge() throws Exception {
// 模拟分片上传
for (int i = 0; i < 5; i++) {
mockMvc.perform(multipart("/api/file/upload")
.file(new MockMultipartFile("file", "chunk.data",
"application/octet-stream", new byte[5*1024*1024]))
.param("chunkNumber", String.valueOf(i))
.param("totalChunks", "5")
.param("identifier", "test123"))
.andExpect(status().isOk());
}
// 触发合并
mockMvc.perform(post("/api/file/merge")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"identifier\":\"test123\",\"fileName\":\"test.txt\"}"))
.andExpect(status().isOk());
// 验证文件存在
assertTrue(Files.exists(Paths.get("/data/uploads/test.txt")));
}
10.2 压力测试指标
我们使用JMeter进行10万分片并发测试:
| 指标 | 测试结果 |
|---|---|
| 平均响应时间 | 128ms |
| 99%线响应时间 | 356ms |
| 错误率 | 0.02% |
| 系统资源占用 | CPU<60%, MEM<4GB |
11. 浏览器兼容方案
11.1 IE8特殊处理
针对IE8的兼容代码:
javascript复制function createXHR() {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
} else if (typeof ActiveXObject !== 'undefined') {
try {
return new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
throw new Error("No XHR implementation available");
}
11.2 分片上传降级方案
当分片API不可用时自动降级:
javascript复制async uploadFallback(file) {
if (this.supportsChunkUpload) {
return this.uploadByChunks(file);
} else {
// 传统表单上传
const form = document.createElement('form');
form.enctype = 'multipart/form-data';
form.method = 'POST';
const input = document.createElement('input');
input.type = 'file';
input.files = [file];
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
12. 运维监控体系
12.1 关键指标监控
建议监控以下指标:
- 分片上传成功率
- 平均分片传输时间
- 合并操作耗时
- 存储空间使用率
- 临时文件数量
12.2 日志分析策略
日志字段示例:
log复制2023-08-01 12:00:00 [INFO] Chunk uploaded -
identifier=abcd123, chunk=5, size=5242880,
duration=120ms, client=192.168.1.100
使用ELK分析日志模式:
json复制{
"timestamp": "2023-08-01T12:00:00Z",
"level": "INFO",
"message": "Chunk uploaded",
"context": {
"identifier": "abcd123",
"chunkNumber": 5,
"fileSize": 5242880,
"durationMs": 120,
"clientIp": "192.168.1.100"
}
}
13. 扩展功能实现
13.1 秒传功能实现
利用文件指纹实现秒传:
java复制public boolean checkFileExists(String fileHash) {
return jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM files WHERE hash = ?",
Integer.class, fileHash) > 0;
}
前端计算文件hash:
javascript复制async calculateFileHash(file) {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
13.2 文件夹上传处理
递归处理文件夹结构:
javascript复制async uploadFolder(folder) {
const entries = folder.createReader().readEntries();
for (const entry of await entries) {
if (entry.isFile) {
await this.uploadFile(await entry.file());
} else if (entry.isDirectory) {
await this.uploadFolder(entry);
}
}
}
14. 客户端SDK封装
14.1 Java客户端示例
java复制public class FileUploadClient {
private static final int CHUNK_SIZE = 5 * 1024 * 1024;
public void upload(File file) throws IOException {
String fileId = UUID.randomUUID().toString();
int totalChunks = (int) Math.ceil(file.length() / (double) CHUNK_SIZE);
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
byte[] buffer = new byte[CHUNK_SIZE];
for (int i = 0; i < totalChunks; i++) {
int bytesRead = raf.read(buffer);
if (bytesRead == -1) break;
uploadChunk(fileId, i, totalChunks,
Arrays.copyOf(buffer, bytesRead));
}
}
mergeFile(fileId, file.getName());
}
}
14.2 进度回调机制
前端进度事件示例:
javascript复制// 分片上传进度事件
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
this.$emit('progress', {
chunk: this.currentChunk,
percent: percent
});
}
};
15. 性能调优记录
15.1 内存优化方案
使用流式处理避免内存溢出:
java复制public void mergeWithStream(String identifier, Path output) throws IOException {
try (OutputStream os = Files.newOutputStream(output)) {
Files.list(Paths.get("/tmp/upload/" + identifier))
.sorted(/*...*/)
.forEach(chunk -> {
try (InputStream is = Files.newInputStream(chunk)) {
is.transferTo(os);
}
});
}
}
15.2 数据库批量操作
使用批量插入提升性能:
java复制@Repository
public class BatchChunkRepository {
@PersistenceContext
private EntityManager em;
public void batchInsert(List<FileChunk> chunks) {
for (int i = 0; i < chunks.size(); i++) {
em.persist(chunks.get(i));
if (i % 100 == 0) {
em.flush();
em.clear();
}
}
}
}
16. 安全审计要点
16.1 文件类型检查
白名单验证文件类型:
java复制private static final Set<String> ALLOWED_TYPES = Set.of(
"image/jpeg", "application/pdf", "text/plain");
public void validateContentType(String contentType) {
if (!ALLOWED_TYPES.contains(contentType)) {
throw new SecurityException("Unsupported file type");
}
}
16.2 文件名消毒处理
防止路径遍历攻击:
java复制public String sanitizeFilename(String filename) {
return filename.replaceAll("[\\\\/]", "_")
.replaceAll("\\.\\.", "_");
}
17. 成本控制实践
17.1 存储成本优化
实施自动清理策略:
sql复制-- 清理30天前的完成记录
DELETE FROM file_transfer_task
WHERE status = 1 AND update_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
17.2 流量节省方案
启用压缩传输:
nginx复制gzip on;
gzip_types application/octet-stream;
gzip_min_length 1024;
18. 移动端适配方案
18.1 微信浏览器支持
处理微信特有问题:
javascript复制function isWeixin() {
return /MicroMessenger/i.test(navigator.userAgent);
}
if (isWeixin()) {
// 微信需要特殊处理分片上传
this.chunkSize = 1 * 1024 * 1024; // 减小分片大小
}
18.2 移动端进度显示
优化移动端UI:
css复制.progress-mobile {
position: fixed;
bottom: 0;
width: 100%;
z-index: 1000;
}
19. 国际化支持
19.1 多语言错误提示
错误码映射:
java复制public enum UploadError {
FILE_TOO_LARGE(1001,
"File too large",
"文件过大"),
INVALID_TYPE(1002,
"Invalid file type",
"文件类型不支持");
private final int code;
private final String enMsg;
private final String zhMsg;
public String getMessage(Locale locale) {
return locale.equals(Locale.CHINA) ? zhMsg : enMsg;
}
}
19.2 时区处理方案
统一使用UTC时间:
java复制@Configuration
public class DateTimeConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setTimeZone(TimeZone.getTimeZone("UTC"));
return mapper;
}
}
20. 项目总结与建议
在实际实施过程中,我们总结了以下几点经验:
- 分片大小选择:5MB在大多数场景下表现最佳,但针对特殊网络环境可动态调整
- 断点续传实现:必须保证分片标识的唯一性和持久性
- 安全防护:文件校验、类型检查、权限控制缺一不可
- 监控体系:建立完善的监控指标及时发现传输异常
对于后续改进方向:
- 增加P2P传输模式降低服务器负载
- 实现客户端加密增强安全性
- 支持更多云存储平台对接
这个方案已在多个央企项目中稳定运行,单日处理文件量超过10TB。核心代码经过多次优化,在保证功能完整性的同时保持了良好的可维护性。