一、为什么要“自动清理”?

Minio 默认“只存不删”,三个月的测试包、过期的临时二维码、同事手抖传上来的 10G 日志,把磁盘撑爆只是时间问题。
手动删?——凌晨 3 点爬起来 mc rm 的酸爽,谁删谁知道。
所以,“让代码替我们熬夜” 才是正道:SpringBoot 定时任务 + Minio,每天凌晨自动扫盘、批量删、失败还能钉钉喊我起床。


二、整体架构(一张图记住)

┌-------------┐     ┌-------------┐     ┌-------------┐
│ SpringBoot  │────▶│   Redis     │────▶│   分布式锁   │  集群只删一次
│  @Scheduled  │     └-------------┘     └-------------┘
│   /Quartz    │              ▲
└------┬-------┘              │
       │ list/delete          │
       ▼                      │
┌-------------┐     ┌-------------┐
│   Minio     │◀────┤  Nacos配置  │  动态改规则 0 重启
└-------------┘     └-------------┘
       ▲
       │ fail
       ▼
┌-------------┐
│ 钉钉机器人   │  失败实时@全员
└-------------┘

三、30 分钟落地:最小可用版本

1. 启动 Minio(Docker 一行)
docker run -d --name minio \
  -p 9000:9000 -p 9001:9001 \
  -v $PWD/data:/data \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

浏览器打开 http://localhost:9001 建个桶 file-bucket

2. SpringBoot 引入依赖
<!-- Minio -->
<dependency>
  <groupId>io.minio</groupId>
  <artifactId>minio</artifactId>
  <version>8.5.2</version>
</dependency>
<!-- 定时任务 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.20</version>
</dependency>
3. 配置 application.yml
minio:
  endpoint: http://localhost:9000
  access-key: minioadmin
  secret-key: minioadmin
  bucket-name: file-bucket
  clean:
    enabled: true
    cron: 0 0 2 * * ?      # 每天 02:00
    expire-days: 30        # 30 天前
    ignore-prefixes: test_,temp_
    max-batch-size: 100
4. 封装 Minio 工具类(核心代码)
@Component
@RequiredArgsConstructor
@Slf4j
public class MinioUtils {
    private final MinioClient minioClient;
    private final MinioConfig cfg;

    /** 列出过期文件 */
    public List<String> listExpired(int days, Set<String> ignore) {
        long expireMillis = days * 24 * 60 * 60 * 1000L;
        long now = System.currentTimeMillis();
        List<String> result = new ArrayList<>();
        minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(cfg.getBucketName())
                .recursive(true).build())
            .forEach(r -> {
                try {
                    Item i = r.get();
                    if (i.isDir()) return;
                    String name = i.objectName();
                    if (ignore.stream().anyMatch(name::startsWith)) return;
                    if (now - i.lastModified().getTime() > expireMillis) {
                        result.add(name);
                    }
                } catch (Exception e) {
                    log.warn("list error", e);
                }
            });
        return result;
    }

    /** 批量删除 */
    public void deleteQuietly(List<String> names, int batch) {
        Lists.partition(names, batch).forEach(part -> {
            List<DeleteObject> dels = part.stream().map(DeleteObject::new).toList();
            minioClient.removeObjects(RemoveObjectsArgs.builder()
                    .bucket(cfg.getBucketName())
                    .objects(dels).build())
                .forEach(r -> {
                    try { r.get(); } catch (Exception e) {
                        log.warn("delete error:{}", e.getMessage());
                    }
                });
        });
    }
}
5. 定时任务(@Scheduled 版)
@Component
@ConditionalOnProperty(prefix = "minio.clean", name = "enabled", havingValue = "true")
@Slf4j
@RequiredArgsConstructor
public class MinioCleanTask {
    private final MinioUtils minioUtils;
    private final MinioConfig config;

    @Scheduled(cron = "${minio.clean.cron}")
    public void clean() {
        var c = config.getClean();
        var expired = minioUtils.listExpired(c.getExpireDays(),
                Set.of(c.getIgnorePrefixes().split(",")));
        if (expired.isEmpty()) return;
        minioUtils.deleteQuietly(expired, c.getMaxBatchSize());
        log.info("clean finish, size={}", expired.size());
    }
}

启动类加 @EnableScheduling,完事。
跑 30 秒 Cron 自测:0/30 * * * * ?,控制台看日志,文件说删就删。


四、生产加固:高可用 4 件套

问题 解决方案 一句话口诀
集群重复执行 Redis 分布式锁 “谁抢到谁干活”
改配置就重启 Nacos + @RefreshScope “配置热刷新”
失败不知道 钉钉/企微机器人 “删失败就@人”
想暂停/改时间 Quartz JDBC 模式 “任务进库,动态改 Cron”

代码全部在原文里,直接 Copy 即可跑,这里只给思路,不再占篇幅。


五、性能小贴士

  1. 大桶先分页
    Minio 的 listObjects 默认 1000 条一页,百万文件时把 maxKeys(500) 调小,防止一次性返回把内存打满。

  2. 删除再并发
    批量删除最多一次 1000 条,建议 100~200 一批;若量特别大,可把 deleteQuietly 用线程池并行删,CPU 打满比 IO 等死强。

  3. 先备份再删
    重要业务桶,清理前把文件 copyObjectbackup-bucket,确认无误后 7 天再删备份,给自己一条“后悔药”。

  4. 灰度放量
    首次上线把 expire-days 设成 180,观察一周没问题再降到 90→60→30,“步子小,不背锅”


六、一行命令快速验证

# 造 1 万个 30 天前的空文件
for i in {1..10000}; do
  touch -d "2024-08-01" /tmp/dummy$i.txt
done
mc cp --recursive /tmp/ minio/file-bucket/30days/
# 30 秒后看日志,1 万文件被扫光

七、结语

把这篇代码丢到你的项目里,10 分钟就能拥有一个不会罢工的“Minio 保洁阿姨”
从此磁盘告警是别人的故事,你只管安心睡觉,让 SpringBoot 和 Minio 在深夜帮你把垃圾带走。
“代码写得早,运维不熬夜!”

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐