Java定时清理流程如何规整

wen java案例 30

本文目录导读:

Java定时清理流程如何规整

  1. 方案选型:按场景分级
  2. 规整代码结构:六层分离法
  3. 规整的必备特性:从能用到好用
  4. 特殊场景:动态调整策略
  5. 规整清单

Java 定时清理流程的“规整”主要体现在架构分层清晰配置高度集中执行逻辑健壮以及具备可观测性

对于不同的应用场景(单机应用 vs. 分布式集群),规整方案有所不同,下面从方案选型代码结构最佳实践三个维度来拆解。

方案选型:按场景分级

场景 推荐方案 核心优势
单机应用 (单体架构) @Scheduled + ScheduledExecutorService 轻量、无需额外依赖、配置简单
分布式集群 (微服务) XXL-Job / Elastic-Job (推荐) 分片执行、故障转移、防止重复执行
云原生 / 无状态 Spring Cloud Task + 消息队列 任务即消息、易于水平扩展、失败重试
数据库级 (存储过程) MySQL Event Scheduler (不推荐 Java 主导) 性能极高,但脱离代码管控

选型建议:如果你的系统尚未引入任何分布式任务调度框架,且没有强一致性的要求,优先选择“@Scheduled + 分布式锁”,在规整度和成本之间取得最佳平衡。


规整代码结构:六层分离法

一个规整的定时清理流程应该包含以下 6 个清晰的层级:

src/main/java/com/example/cleaner/
├── config                  # 配置层
│   ├── CleanerProperties   # 所有清理相关的配置(开关、保留天数、执行时间)
│   └── ScheduleConfig      # 调度配置(如线程池大小)
├── task                    # 任务层(调度入口)
│   ├── DataCleanTask       # @Scheduled 注解入口
│   └── DataCleanJob        # 分布式任务(如 XXL-Job 处理器)
├── executor                # 执行器(业务逻辑层)
│   └── LogCleanExecutor    # 清理日志的实际逻辑
├── service                 # 服务层
│   └── LogCleanService     # 复杂的清理查询/删除逻辑
├── strategy                # 策略层(可扩展性)
│   ├── CleanStrategy       # 清理策略接口
│   ├── DoNothingStrategy   # 什么都不做(开关关闭时使用)
│   └── DeleteStrategy      # 物理删除
├── record                  # 记录层(可观测性)
│   └── CleanRecordManager  # 记录清理结果到日志/Metrics/数据库

配置层:拒绝硬编码

规整的第一步,所有参数不能写在代码里,必须写在 application.yml 中:

cleaner:
  enabled: true                      # 总开关
  log:
    retention-days: 30               # 保留天数
    batch-size: 1000                 # 每批次删除数量
    cron: "0 0 2 * * ?"              # 凌晨2点执行
  file:
    enabled: false                   # 关闭文件清理

任务层:极简调度

任务层只做两件事:检查开关调用执行器

@Component
public class DataCleanTask {
    @Autowired
    private CleanerProperties properties;
    @Autowired
    private LogCleanExecutor logCleanExecutor;
    @Scheduled(cron = "${cleaner.log.cron}")
    public void executeLogClean() {
        if (!properties.isEnabled()) return;  // 总开关控制
        if (!properties.getLog().isEnabled()) return; // 模块开关控制
        logCleanExecutor.execute();
    }
}

执行器层:真正的逻辑编排

规整的执行器具备分批处理失败重试统计结果的能力。

@Service
public class LogCleanExecutor {
    @Autowired
    private LogCleanService logCleanService;
    @Autowired
    private CleanRecordManager recordManager;
    public void execute() {
        int totalDeleted = 0;
        boolean hasMore = true;
        int batchSize = properties.getLog().getBatchSize(); // 1000
        int retryCount = 3; // 失败重试次数
        while (hasMore) {
            try {
                // 1. 分批查询需要删除的ID
                List<Long> ids = logCleanService.findIdsToDelete(batchSize);
                if (ids.isEmpty()) {
                    hasMore = false;
                } else {
                    // 2. 执行删除(带重试)
                    int deleted = retryableDelete(ids, retryCount);
                    totalDeleted += deleted;
                    log.info("Deleted {} records, total: {}", deleted, totalDeleted);
                }
            } catch (Exception e) {
                log.error("Clean failed in batch, stop to prevent infinite loop", e);
                break; // 避免死循环
            }
        }
        // 3. 记录完成
        recordManager.recordCleanResult("LOG_CLEAN", totalDeleted);
    }
    private int retryableDelete(List<Long> ids, int retryCount) {
        for (int i = 0; i < retryCount; i++) {
            try {
                return logCleanService.batchDelete(ids);
            } catch (Exception e) {
                log.warn("Delete failed, retry {}/{}", i+1, retryCount);
                Thread.sleep(100L * (i+1)); // 指数退避
            }
        }
        throw new RuntimeException("Failed to delete after retries");
    }
}

规整的必备特性:从能用到好用

幂等性(防止重复执行)

在分布式环境下,@Scheduled 会导致多个节点同时执行,规整的做法是使用MySQL行锁或者Redis分布式锁

// 使用 Redis 分布式锁(推荐)
public void execute() {
    String lockKey = "cleaner:log:lock";
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", Duration.ofMinutes(30));
    if (!locked) {
        log.info("Clean task already running on another node, skip");
        return;
    }
    try {
        // 实际清理逻辑
    } finally {
        redisTemplate.delete(lockKey);
    }
}

优雅关闭(不残留事务)

Spring 关闭时,正在执行的清理任务应该能执行完当前批次再停止。

@Bean(destroyMethod = "shutdown")
public Executor taskScheduler() {
    ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5);
    // 设置等待队列中的任务执行完毕再关闭
    executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(true);
    return executor;
}

可观测性(不黑盒)

规整的清理流程必须能被监控到:

  • 日志:清理开始、结束、每次批次、异常都打印日志。
  • Metrics:清理耗时、清理数量、失败次数(接入 Micrometer / Prometheus)。
  • 告警:连续失败 N 次或清理 0 条但数据库中有大量过期数据时,发出告警。
public void recordCleanResult(String taskName, int deletedCount) {
    // 记录到日志
    log.info("Clean task [{}] completed, deleted {} records", taskName, deletedCount);
    // 记录到 Prometheus Metrics
    counter.cleanCount.labels(taskName).inc(deletedCount);
    // 记录到数据库(可选)
    cleanRecordRepository.save(new CleanRecord(taskName, deletedCount, new Date()));
}

数据量过大时的特殊处理

如果是一次性清理千万级数据,务必使用索引下推 + 分批删除,避免锁表和主从延迟。

-- 推荐做法:先查主键,再根据主键删除
DELETE FROM log_table WHERE id IN (
    SELECT id FROM (
        SELECT id FROM log_table 
        WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY) 
        LIMIT 1000
    ) AS tmp
);

特殊场景:动态调整策略

如果你希望运行时动态修改清理策略(比如临时改为归档而非删除),可以使用策略模式

@Component
public class CleanStrategyContext {
    @Autowired 
    private Map<String, CleanStrategy> strategyMap; // Spring 会注入继承该接口的所有 Bean
    public void execute(String strategyName) {
        CleanStrategy strategy = strategyMap.getOrDefault(strategyName, new DoNothingStrategy());
        strategy.clean();
    }
}

配置中心(Nacos / Apollo)修改 cleaner.log.strategy=archive 时,代码自动切换到归档策略。


规整清单

一个“规整”的 Java 定时清理流程应该满足以下几点:

维度 标准
配置 所有参数在配置中心 / yml 中,支持运行时动态修改
调度 单机用 @Scheduled,集群用 XXL-Job + 锁
执行 分批处理、失败重试、优雅停止
幂等 使用分布式锁保证同一时间只有一个节点执行
监控 有日志、有 Metrics、有告警
扩展 使用策略模式支持不同清理方式(删除 / 归档 / 迁移)

按照这个结构搭建,清理流程不仅稳定可靠,还便于维护和排障。

抱歉,评论功能暂时关闭!