CC 咖啡猫的工作空间 Coding Space

文件云存储实践

文件云存储涉及上传、下载、管理和安全,是大多数系统的基础能力。


1. 存储方案对比

方案 优点 缺点 适用场景
本地磁盘 简单、无额外费用 扩展性差、不支持跨机器 轻量单机部署
NFS共享存储 多节点共享 性能一般 中小型集群
MinIO S3兼容、私有部署、数据自控 有数据合规要求、自建云
阿里OSS 托管、高可用、功能丰富 生产环境首选
腾讯COS 同上 腾讯云用户
七牛/又拍云 CDN加速强 静态资源为主

2. MinIO 详解

2.1 什么是 MinIO

MinIO 是一款高性能的对象存储系统,兼容 Amazon S3 API。采用 Go 语言开发,单节点即可支持 TB 级存储,分布式部署时可达 PB 级(软件安装在自己的服务器里,数据存在自己建立的服务器上)。

核心特点:

  • S3 兼容:使用与 AWS S3 相同的 API 和 SDK,现有 S3 工具链可直接迁移
  • 高性能:读写性能优秀,吊起时间是毫秒级
  • 轻量部署:单二进制文件,无依赖,适合 K8s 环境
  • 开源免费:Apache License v2.0

2.2 为什么数据合规场景选 MinIO

合规要求 MinIO 如何满足
数据主权 数据存在自己的服务器,不在第三方云厂商
等保/密评 私有化部署可以通过等保测评,不依赖外部审计
行业监管 金融、医疗、政务等行业要求数据不出网/不出域
跨境合规 避免数据存储在境外服务器
降低风险 不会被云厂商锁定,数据可完整迁移
云厂商存储(阿里OSS等)数据实际存储在厂商的服务器上,
即使购买了"独立存储桶",数据管理权仍在厂商手中。
MinIO 部署在自己的基础设施,数据完全自控。

2.3 MinIO 适用场景

场景 说明
私有云/混合云 企业内部云环境,数据不出域
K8s 原生 天然的容器化支持,Operator 模式管理集群
数据湖底座 作为 Spark、Hive 等大数据平台的存储后端
备份归档 高可靠性的冷数据存储
多云架构 避免厂商绑定,跨云统一存储接口

2.4 MinIO vs 云厂商存储对比

维度 MinIO 阿里OSS / 腾讯COS
部署方式 私有部署,自建服务器 托管服务,厂商运维
数据控制 完全自控,数据在自己的服务器 数据在云厂商服务器
费用 服务器成本(硬件+运维) 按量付费(存储+流量)
运维成本 需要专业运维团队 厂商负责,可靠性高
S3 兼容性 完全兼容 S3 兼容(但有扩展功能)
功能丰富度 基础功能完善 丰富(CDN、镜像、生命周期管理等)
适用场景 有合规要求、数据不出域 一般业务、想省心

2.5 企业存储方案选择

┌─────────────────────────────────────────────┐
│              企业存储方案选择                  │
├─────────────────────────────────────────────┤
│                                             │
│  一般业务 / 中小企业                          │
│  → 直接用云厂商对象存储(阿里OSS/腾讯COS)     │
│  → 省事、低成本、功能丰富                      │
│                                             │
│  有数据合规要求 / 政务金融医疗                 │
│  → MinIO 私有化部署                          │
│  → 数据完全自控,满足监管要求                  │
│                                             │
│  多云策略 / 避免厂商绑定                      │
│  → MinIO + 云厂商存储组合                    │
│                                             │
│  成本优化 / 大数据场景                       │
│  → MinIO 做数据湖 + 云厂商做分发              │
│                                             │
└─────────────────────────────────────────────┘

结论:没有特殊需求用云厂商存储,有合规或隐私要求用 MinIO。多数企业初期用一种方案,复杂场景按需组合。


3. 设计要点

3.1 核心模型

// 文件元数据
public class FileMetadata {
    private String fileId;          // 文件ID(UUID)
    private String originalName;    // 原始文件名
    private String storageName;    // 存储文件名(UUID + 后缀)
    private String storagePath;    // 存储路径
    private Long fileSize;         // 文件大小(字节)
    private String contentType;    // MIME类型
    private String md5;            // 文件MD5(校验用)
    private String uploaderId;     // 上传者ID
    private LocalDateTime uploadedAt;  // 上传时间
    private LocalDateTime expiredAt;   // 过期时间(可选)
}

// 存储策略(多云支持)
public enum StorageType {
    LOCAL, OSS, COS, MINIO
}

public interface FileStorageService {
    String upload(MultipartFile file, String bucket);
    void delete(String bucket, String objectKey);
    URL generatePresignedUrl(String bucket, String objectKey);
    InputStream download(String bucket, String objectKey);
}

3.2 文件表设计

CREATE TABLE sys_file (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    file_id         VARCHAR(64) UNIQUE NOT NULL COMMENT '文件唯一标识',
    original_name   VARCHAR(255) NOT NULL COMMENT '原始文件名',
    storage_name    VARCHAR(255) NOT NULL COMMENT '存储文件名',
    storage_path     VARCHAR(500) NOT NULL COMMENT '存储路径',
    file_size       BIGINT COMMENT '文件大小(字节)',
    content_type    VARCHAR(100) COMMENT 'MIME类型',
    file_md5        VARCHAR(64) COMMENT '文件MD5',
    bucket          VARCHAR(100) DEFAULT 'default' COMMENT '存储桶',
    storage_type    TINYINT COMMENT '存储类型 1-本地 2-OSS 3-COS 4-MinIO',
    uploader_id     BIGINT COMMENT '上传人ID',
    upload_time     DATETIME DEFAULT CURRENT_TIMESTAMP,
    expire_time     DATETIME COMMENT '过期时间',
    deleted         TINYINT DEFAULT 0,
    PRIMARY KEY (id),
    INDEX idx_file_id (file_id),
    INDEX idx_uploader (uploader_id)
) COMMENT '文件表';

3. 实现方案

3.1 阿里OSS实现

@Service
public class OssFileStorageService implements FileStorageService {

    @Autowired
    private OSSProperties ossProperties;

    @Override
    public String upload(MultipartFile file, String bucket) {
        String fileId = UUID.randomUUID().toString().replace("-", "");
        String extension = getExtension(file.getOriginalFilename());
        String storageName = fileId + "." + extension;

        OSS ossClient = new OSSClientBuilder().build(
            ossProperties.getEndpoint(),
            ossProperties.getAccessKeyId(),
            ossProperties.getAccessKeySecret()
        );

        ossClient.putObject(bucket, storageName, file.getInputStream());

        // 入库元数据
        saveFileMetadata(file, storageName, bucket);

        return fileId;
    }

    @Override
    public URL generatePresignedUrl(String bucket, String objectKey) {
        OSS ossClient = createClient();
        Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
        return ossClient.generatePresignedUrl(bucket, objectKey, expiration);
    }
}

3.2 分片上传(大文件)

// 前端分片上传,后端合并
@PostMapping("/upload/chunk")
public Result<String> uploadChunk(
        @RequestParam("file") MultipartFile chunk,
        @RequestParam("uploadId") String uploadId,
        @RequestParam("chunkIndex") int chunkIndex) {

    // 1. 创建分片文件目录
    Path chunkDir = Paths.get("/data/chunks", uploadId);
    Files.createDirectories(chunkDir);

    // 2. 保存分片
    Path chunkPath = chunkDir.resolve("part_" + chunkIndex);
    chunk.transferTo(chunkPath.toFile());

    // 3. 检查是否所有分片都已上传
    if (isAllChunksUploaded(uploadId, totalChunks)) {
        // 4. 合并分片
        mergeChunks(uploadId, targetFileName);
        // 5. 清理分片目录
        deleteChunkDir(uploadId);
        return Result.success("上传完成");
    }

    return Result.success("分片已接收");
}

3.3 策略模式选择存储

@Service
public class FileStorageFactory {

    @Autowired
    private Map<String, FileStorageService> storageServices;

    public FileStorageService getStorage(StorageType type) {
        return storageServices.get(type.name().toLowerCase() + "FileStorageService");
    }

    public FileStorageService getDefaultStorage() {
        // 根据配置返回默认存储
        String defaultType = properties.getDefaultStorage();
        return getStorage(StorageType.valueOf(defaultType.toUpperCase()));
    }
}

4. 安全考虑

安全措施 说明
文件类型校验 前端校验扩展名 + 后端检测文件魔数(不能用扩展名判断)
文件大小限制 单文件大小、可接受的内容类型
私有桶签名URL 敏感文件不暴露直链,用预签名URL
病毒扫描 上传后触发病毒扫描,异步通知结果
下载审计 记录谁下载了什么文件
CDN鉴权 Type A/B/C 鉴权防止链接盗用

5. 常见问题

问题 解决方案
上传超时 分片上传 + 断点续传
文件重复上传 MD5去重,已存在则返回已有fileId
存储空间不足 定期清理过期文件 + 告警
直链被盗用 私有文件走签名URL,不使用永久链接

最后更新:2026/05/11