设计模式与定时任务:命令模式封装任务、策略模式选择调度策略、观察者监控任务
定时任务,说白了就是让程序在指定时间或周期性地干活。我做了这么多年系统,几乎每个项目都离不开它——从定时报表、数据同步,到心跳检测、缓存刷新。但很多人写定时任务,就是往一个循环里塞逻辑,结果代码越改越乱,最后自己都不敢动。
今天咱们聊聊怎么用设计模式把定时任务写得优雅。我习惯把三个模式组合起来用:命令模式封装任务、策略模式选择调度策略、观察者模式监控任务。这套组合拳,我在好几个高并发项目里验证过,效果不错。
为什么需要设计模式来管理定时任务?
先看一个反面教材。我曾经接手过一个老项目,定时任务代码长这样:
// 反面示例:所有逻辑揉在一起
public class TimerTaskRunner {
public void run() {
// 任务A:清理过期数据
// 任务B:发送邮件通知
// 任务C:生成统计报表
// 调度逻辑:固定间隔5分钟
// 监控逻辑:打印日志
}
}
你想想看,如果哪天要新增一个任务,或者把固定间隔改成cron表达式,或者想加个告警通知——都得改这个类。单一职责原则?不存在的。开闭原则?更别提了。
所以我们需要把「任务是什么」「什么时候执行」「执行得怎么样」这三个关注点拆开。这就是三个设计模式各自要解决的问题。
命令模式:把任务封装成对象
命令模式的核心思想,就是把一个请求或操作封装成一个对象。这样你就可以把任务当作参数传递、排队、记录日志,甚至支持撤销。
我习惯先定义一个抽象接口:
// 命令接口:所有定时任务都实现这个接口
public interface TaskCommand {
void execute();
String getTaskName();
TaskResult getLastResult();
}
然后每个具体任务实现这个接口。比如清理过期数据的任务:
public class CleanupTask implements TaskCommand {
private TaskResult lastResult;
@Override
public void execute() {
try {
// 实际清理逻辑
System.out.println("清理过期数据...");
lastResult = TaskResult.success("清理完成");
} catch (Exception e) {
lastResult = TaskResult.failure(e.getMessage());
}
}
@Override
public String getTaskName() {
return "数据清理任务";
}
@Override
public TaskResult getLastResult() {
return lastResult;
}
}
这样做的好处是什么?每个任务都是独立的对象,你可以把它们放到List里统一管理,也可以序列化后持久化到数据库。我在一个分布式调度系统里,就是用命令模式把任务序列化成JSON,通过网络发送给不同的worker节点执行。
策略模式:灵活切换调度策略
任务封装好了,接下来就是「什么时候执行」。不同的业务场景需要不同的调度策略:有的要固定间隔,有的要cron表达式,有的要延迟执行,还有的要按优先级调度。
策略模式正好解决这个问题。定义一个调度策略接口:
// 调度策略接口
public interface ScheduleStrategy {
long getNextDelay(); // 返回下一次执行的延迟时间(毫秒)
boolean shouldContinue(); // 是否继续调度
}
然后实现几种常见的策略:
// 固定间隔策略
public class FixedRateStrategy implements ScheduleStrategy {
private final long intervalMs;
public FixedRateStrategy(long intervalMs) {
this.intervalMs = intervalMs;
}
@Override
public long getNextDelay() {
return intervalMs;
}
@Override
public boolean shouldContinue() {
return true;
}
}
// Cron表达式策略
public class CronStrategy implements ScheduleStrategy {
private final CronExpression cronExpression;
private Date lastFireTime;
public CronStrategy(String cron) {
this.cronExpression = new CronExpression(cron);
}
@Override
public long getNextDelay() {
Date next = cronExpression.getNextValidTimeAfter(
lastFireTime == null ? new Date() : lastFireTime);
lastFireTime = next;
return next.getTime() - System.currentTimeMillis();
}
@Override
public boolean shouldContinue() {
return true;
}
}
调度器本身不关心具体策略,它只负责调用策略接口:
public class TaskScheduler {
private final TaskCommand task;
private final ScheduleStrategy strategy;
private final ScheduledExecutorService executor;
public TaskScheduler(TaskCommand task, ScheduleStrategy strategy) {
this.task = task;
this.strategy = strategy;
this.executor = Executors.newSingleThreadScheduledExecutor();
}
public void start() {
scheduleNext();
}
private void scheduleNext() {
if (!strategy.shouldContinue()) return;
executor.schedule(() -> {
task.execute();
scheduleNext(); // 递归调度下一次
}, strategy.getNextDelay(), TimeUnit.MILLISECONDS);
}
}
你看,调度器、任务、策略三者完全解耦。想换调度方式?换个策略对象就行。我在项目中遇到过需求变更,从固定间隔改成cron表达式,只改了一行配置代码。
观察者模式:监控任务执行状态
任务在跑,但跑得怎么样?有没有失败?有没有超时?这些信息需要及时通知到监控系统。观察者模式就是干这个的。
定义一个观察者接口:
// 观察者接口
public interface TaskObserver {
void onTaskStarted(TaskCommand task);
void onTaskCompleted(TaskCommand task, TaskResult result);
void onTaskFailed(TaskCommand task, Throwable error);
}
然后实现具体的观察者,比如日志观察者、告警观察者:
// 日志观察者
public class LoggingObserver implements TaskObserver {
@Override
public void onTaskStarted(TaskCommand task) {
System.out.println("[" + LocalDateTime.now() + "] 任务开始: " + task.getTaskName());
}
@Override
public void onTaskCompleted(TaskCommand task, TaskResult result) {
System.out.println("[" + LocalDateTime.now() + "] 任务完成: " + task.getTaskName() + " - " + result.getMessage());
}
@Override
public void onTaskFailed(TaskCommand task, Throwable error) {
System.err.println("[" + LocalDateTime.now() + "] 任务失败: " + task.getTaskName() + " - " + error.getMessage());
}
}
// 告警观察者(发送邮件或短信)
public class AlertObserver implements TaskObserver {
@Override
public void onTaskFailed(TaskCommand task, Throwable error) {
// 发送告警通知
System.out.println("发送告警: 任务 " + task.getTaskName() + " 执行失败!");
}
}
然后在调度器里集成观察者:
public class ObservableTaskScheduler extends TaskScheduler {
private final List<TaskObserver> observers = new ArrayList<>();
public ObservableTaskScheduler(TaskCommand task, ScheduleStrategy strategy) {
super(task, strategy);
}
public void addObserver(TaskObserver observer) {
observers.add(observer);
}
@Override
protected void executeTask() {
notifyStarted();
try {
task.execute();
notifyCompleted(task.getLastResult());
} catch (Exception e) {
notifyFailed(e);
}
}
private void notifyStarted() {
observers.forEach(o -> o.onTaskStarted(task));
}
private void notifyCompleted(TaskResult result) {
observers.forEach(o -> o.onTaskCompleted(task, result));
}
private void notifyFailed(Throwable error) {
observers.forEach(o -> o.onTaskFailed(task, error));
}
}
整体架构图
下面这张图展示了三个模式如何协同工作:
调度器是核心,它持有任务对象(命令模式)、策略对象(策略模式),并通知观察者(观察者模式)。三者各司其职,互不干扰。
实战中的避坑指南
这套方案我用了好几年,踩过一些坑,分享给你:
- 任务执行时间超过调度间隔怎么办? 我曾经遇到过清理任务跑了10分钟,但调度间隔是5分钟,结果任务堆积。解决方案:用
FixedDelay代替FixedRate,或者加一个「是否正在执行」的标志位。 - 观察者太多影响性能? 我建议用异步通知。把观察者的回调放到一个独立线程池里,或者用消息队列解耦。
- 策略切换时任务状态丢失? 如果任务正在执行,突然切换策略,可能会出问题。我习惯在切换前先停止调度器,等当前任务执行完再重新启动。
- 分布式环境下任务重复执行? 命令模式的任务对象最好实现幂等性。同一个任务执行两次,结果应该一样。
好了,今天就聊到这儿。这套组合拳你可以在自己的项目里试试,有什么问题欢迎交流。