本文目录导读:

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、有告警 |
| 扩展 | 使用策略模式支持不同清理方式(删除 / 归档 / 迁移) |
按照这个结构搭建,清理流程不仅稳定可靠,还便于维护和排障。