MinIO自建对象存储,省OSS费用的完整方案

背景

做外包项目经常遇到文件上传需求------用户头像、合同PDF、Excel导出文件。一开始图省事用阿里云OSS,结果有个项目用户上传了几千份合同扫描件,一个月OSS费用干到200多。

200块不算多,但积少成多,三个客户的项目加起来一年几千块。而我的服务器本来就剩200G磁盘闲着,于是全部切到MinIO自建。

MinIO是什么

MinIO是开源的对象存储,兼容Amazon S3 API。部署在你的服务器上,Docker一个容器搞定。代码里原来怎么写S3,现在就怎么写MinIO------API一模一样。

Docker部署

bash 复制代码
services:
  minio:
    image: minio/minio:latest
    container_name: minio
    ports:
      - "9000:9000"   # API端口
      - "9001:9001"   # 控制台端口
    volumes:
      - ./minio/data:/data
      - ./minio/config:/root/.minio
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=your_password_here
    command: server /data --console-address ":9001"
    networks:
      - aipdt-cloud
    restart: unless-stopped

启动后用 http://你的IP:9001 登录控制台,创建Bucket。建议至少建两个:public 放公开文件(头像之类的),private 放私密文件(合同、证件)。

SpringBoot接入

依赖就一个:

xml 复制代码
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>

配置文件:

vbnet 复制代码
minio:
  endpoint: http://minio:9000      # 容器内直接用服务名
  access-key: admin
  secret-key: your_password_here
  bucket-public: public
  bucket-private: private

配置类:

kotlin 复制代码
@Configuration
public class MinioConfig {
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.access-key}")
    private String accessKey;
    @Value("${minio.secret-key}")
    private String secretKey;
​
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

工具类

把常用操作封装一个工具类,业务代码里一行搞定:

typescript 复制代码
@Slf4j
@Component
public class MinioUtil {
    @Autowired
    private MinioClient minioClient;
​
    // 上传文件
    public String upload(String bucket, InputStream inputStream, 
                         String fileName, long size) {
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(fileName)
                    .stream(inputStream, size, -1)
                    .contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)
                    .build());
            return fileName;
        } catch (Exception e) {
            log.error("上传失败", e);
            throw new RuntimeException("文件上传失败");
        }
    }
​
    // 获取文件流
    public InputStream download(String bucket, String fileName) {
        try {
            return minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(fileName)
                    .build());
        } catch (Exception e) {
            log.error("下载失败", e);
            throw new RuntimeException("文件下载失败");
        }
    }
​
    // 生成临时访问链接(7天有效)
    public String presignedUrl(String bucket, String fileName) {
        try {
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .bucket(bucket)
                    .object(fileName)
                    .expiry(7, TimeUnit.DAYS)
                    .method(Method.GET)
                    .build());
        } catch (Exception e) {
            log.error("生成链接失败", e);
            return null;
        }
    }
​
    // 删除文件
    public void delete(String bucket, String fileName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucket)
                    .object(fileName)
                    .build());
        } catch (Exception e) {
            log.error("删除失败", e);
        }
    }
}

业务代码示例

上传头像:

less 复制代码
@PostMapping("/avatar")
public Result<String> uploadAvatar(@RequestParam MultipartFile file) {
    String fileName = "avatar/" + userId + ".jpg";
    minioUtil.upload(bucketPublic, file.getInputStream(), 
                     fileName, file.getSize());
    String url = minioUtil.presignedUrl(bucketPublic, fileName);
    return Result.ok(url);
}

导出Excel存MinIO:

ini 复制代码
// 生成Excel
ByteArrayOutputStream out = new ByteArrayOutputStream();
excelWriter.write(out);
​
// 上传到MinIO
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
String fileName = "excel/" + UUID.randomUUID() + ".xlsx";
minioUtil.upload(bucketPrivate, in, fileName, out.size());
​
// 返回下载链接
String downloadUrl = minioUtil.presignedUrl(bucketPrivate, fileName);

访问策略

公开文件:头像、封面图等,Bucket设置为公开读:

arduino 复制代码
mc policy set download myminio/public

私密文件:合同、身份证等,Bucket保持私有,通过 presigned URL 控制访问,链接带过期时间和签名,安全可控。

踩过的坑

1. 容器内用localhost连不上

MinIO和SpringBoot都在Docker里时,endpoint 不能用 localhost。配容器名 http://minio:9000,或用宿主机IP。

2. 上传大文件OOM

putObject 默认把整个文件读进内存。上传几十M的文件时内存会爆。换成分片上传:

less 复制代码
minioClient.putObject(PutObjectArgs.builder()
        .bucket(bucket)
        .object(fileName)
        .stream(inputStream, -1, 5 * 1024 * 1024)  // 5M分片
        .build());

第三个参数 -1 表示未知总大小,第四个参数 5M 是分片大小。适合前端直传的场景。

3. presigned URL 的域名问题

MinIO生成的链接默认是 http://内网IP:9000/bucket/file。如果前端需要访问,链接里得是外网域名。配nginx代理:

bash 复制代码
location /minio/ {
    proxy_pass http://minio:9000/;
}

或者在MinIO启动时指定环境变量 MINIO_SERVER_URL=https://你的域名

什么时候不适合MinIO

  • 需要CDN加速:OSS自带CDN,MinIO得自己加一层nginx反向代理和CDN配置
  • 高可用刚需:单机挂了就不可用。MinIO也支持集群模式,但你得搞多台服务器
  • 合规要求高:比如等保三级要求对象存储有审计日志、跨区域备份,自建搞起来麻烦

以上三条都不满足的话,MinIO够用了。

总结

  1. MinIO替代OSS,Docker一键部署,S3 API兼容
  2. public/private两个Bucket,公开读+预签名URL两种访问模式
  3. 大文件用分片上传,避免OOM
  4. 对CDN和高可用没要求的中小型项目,MinIO完全够用
  5. 一年省几千块OSS费用,一台服务器资源充分利用

我正以"一个人+AI"模式做独立开发,微信搜索小程序"面狮狮"体验AI模拟面试。更多实战经验:gitee.com/yao113088/j...

相关推荐
Ai拆代码的曹操1 小时前
深夜告警风暴:Dubbo 异步调用回调丢失 300 次——一个 tech lead 的自述
后端·dubbo
2501_933923251 小时前
设计网页的时候加载不出来怎么办?认识常见的状态码
前端·spring
only-lucky1 小时前
QML深入学习五(Controls模块)
前端·javascript·学习
程序员黑豆1 小时前
鸿蒙应用开发之路由:Router 页面路由使用教程
前端·harmonyos
gyx_这个杀手不太冷静2 小时前
Agent开发进阶指南(第 2 章):Agent 运行全流程拆解、上下文窗口、流式输出、记忆系统与 Function Call 实战
前端·架构·agent
anyup2 小时前
像这种问题千万别自己动手,否则你可太看不起 AI 了
前端·架构·trae
hunterandroid2 小时前
[鸿蒙从零到一] HarmonyOS Web 组件与 JSBridge 通信实战:从页面加载到安全协议
前端
Codelinghu3 小时前
LangSmith Evaluate实战评估Agent,不要在玩Demo了
后端
xingren3 小时前
「拍摄器」UI 特效 - 在 Winform/WPF/WinUI3/Avalonia/Web 的实现
前端