如何设计一个文件存储系统?

2025年 阅读约 15 分钟 面试指南 · 系统设计

深入解析文件存储系统设计:对象存储(MinIO/S3协议)、分块上传(大文件优化)、断点续传、秒传机制(MD5去重)、CDN加速,附面试模拟问答。

一句话总结

文件存储系统核心功能:上传(普通上传、分块上传、断点续传)、下载(直接下载、CDN加速)、秒传(MD5去重,相同文件不重复存储)。存储方案:对象存储(MinIO/S3,推荐)vs 本地文件系统(简单但不支持分布式)vs FastDFS(传统方案)。大文件用分块上传(每块5MB,并发上传,最后合并),配合断点续传(记录已上传分块,重传时跳过)。

初级理解

存储方案对比

方案原理优点缺点
本地存储文件存服务器磁盘简单无法分布式、扩容难
FastDFS分布式文件系统轻量、适合小文件社区不活跃、功能有限
MinIO对象存储(S3兼容)高性能、S3兼容、云原生需要额外部署
云OSS阿里云OSS/腾讯COS免运维、高可用成本高
# MinIO 基本使用 # 安装 docker run -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USER=admin \ -e MINIO_ROOT_PASSWORD=admin123 \ minio/minio server /data --console-address ":9001" # Java SDK 上传 MinioClient client = MinioClient.builder() .endpoint("http://localhost:9000") .credentials("admin", "admin123") .build(); client.putObject( PutObjectArgs.builder() .bucket("my-bucket") .object("file.jpg") .stream(inputStream, fileSize, -1) .build() );
一句话总结:对象存储(MinIO/S3)是主流方案,支持分布式、高可用。

中级深入

分块上传 + 断点续传

# 分块上传流程 # 1. 初始化:生成 uploadId,确定分块大小(5MB) # 2. 分块上传:并发上传各分块 # 3. 完成:通知服务端合并分块 # 服务端接口 # POST /upload/init # { "fileName": "video.mp4", "fileSize": 104857600, "chunkSize": 5242880 } # → { "uploadId": "xxx", "chunkCount": 20 } # POST /upload/chunk # { "uploadId": "xxx", "chunkIndex": 0, "file": binary } # → { "success": true } # POST /upload/complete # { "uploadId": "xxx" } # → { "url": "https://cdn.example.com/video.mp4" } # 断点续传 # GET /upload/progress?uploadId=xxx # → { "uploadedChunks": [0, 1, 2, 5, 6] } # 客户端跳过已上传的分块,只传缺失的

秒传机制

# 秒传原理:文件 MD5 去重 # 1. 客户端计算文件 MD5 # 2. 上传前先查服务端是否已有该 MD5 # 3. 有 → 直接返回文件 URL(秒传) # 4. 无 → 正常上传 # 服务端 # POST /upload/check # { "md5": "d41d8cd98f00b204e9800998ecf8427e" } # → { "exists": true, "url": "https://cdn.example.com/file.jpg" } # 数据库设计 CREATE TABLE file_info ( id BIGINT PRIMARY KEY, file_name VARCHAR(255), file_size BIGINT, md5 VARCHAR(32) UNIQUE, url VARCHAR(500), created_at DATETIME );
中级要点:大文件分块上传(5MB/块)+ 断点续传;秒传用 MD5 去重。

高级拓展

CDN 加速

# CDN 加速架构 # 用户 → CDN 边缘节点(就近访问) # ↓ 缓存未命中 # 源站(MinIO/OSS) # ↓ # 返回文件 + 缓存到 CDN # CDN 缓存策略 # 静态资源(图片/CSS/JS):缓存 30 天 # 用户上传文件:缓存 7 天 # 热点文件:预热到 CDN # 回源策略 # CDN 缓存过期 → 回源站拉取 # 源站设置 Cache-Control 头控制 CDN 缓存时间

图片处理

# 图片处理需求 # 1. 缩略图:原图 → 多种尺寸 # 2. 水印:添加文字/图片水印 # 3. 格式转换:jpg → webp # 4. 裁剪:按比例/指定区域裁剪 # 方案1:上传时处理(预生成) # 上传 → 生成多种尺寸 → 存储 # 优点:访问快;缺点:存储空间大 # 方案2:访问时处理(实时) # 访问 URL 带参数:/image.jpg?w=200&h=200 # CDN/网关实时处理并缓存 # 优点:灵活;缺点:首次访问慢 # 方案3:混合(推荐) # 常用尺寸预生成 + 非常用尺寸实时处理

实战场景

场景:视频上传服务

@Service public class VideoUploadService { @Autowired private MinioClient minioClient; # 初始化分块上传 public UploadInitVO initUpload(String fileName, long fileSize) { String uploadId = UUID.randomUUID().toString(); int chunkSize = 5 * 1024 * 1024; # 5MB int chunkCount = (int) Math.ceil((double) fileSize / chunkSize); # 记录上传任务 redis.opsForHash().put("upload:" + uploadId, "fileName", fileName); redis.opsForHash().put("upload:" + uploadId, "chunkCount", chunkCount); redis.expire("upload:" + uploadId, 24, TimeUnit.HOURS); return new UploadInitVO(uploadId, chunkCount, chunkSize); } # 上传分块 public void uploadChunk(String uploadId, int chunkIndex, byte[] data) { String chunkKey = "chunks/" + uploadId + "/" + chunkIndex; minioClient.putObject(PutObjectArgs.builder() .bucket("video-uploads") .object(chunkKey) .stream(new ByteArrayInputStream(data), data.length, -1) .build()); # 记录已上传分块 redis.opsForSet().add("upload:chunks:" + uploadId, String.valueOf(chunkIndex)); } }

面试模拟

面试官:大文件上传怎么设计?

你:分块上传:将大文件切成5MB的块,并发上传,最后合并。配合断点续传:记录已上传分块,重传时跳过。秒传:上传前计算MD5,服务端已有则直接返回URL。存储用MinIO(S3兼容对象存储),配合CDN加速下载。

面试官:秒传怎么实现?

你:客户端计算文件MD5,上传前先请求服务端检查MD5是否存在。存在则直接返回已有文件URL(秒传),不存在则正常上传。数据库MD5字段建唯一索引。注意:MD5有碰撞概率,重要场景可加文件大小双重校验。