设计模式与定时任务:命令模式封装任务、策略模式选择调度策略、观察者监控任务

定时任务,说白了就是让程序在指定时间或周期性地干活。我做了这么多年系统,几乎每个项目都离不开它——从定时报表、数据同步,到心跳检测、缓存刷新。但很多人写定时任务,就是往一个循环里塞逻辑,结果代码越改越乱,最后自己都不敢动。

今天咱们聊聊怎么用设计模式把定时任务写得优雅。我习惯把三个模式组合起来用:命令模式封装任务策略模式选择调度策略观察者模式监控任务。这套组合拳,我在好几个高并发项目里验证过,效果不错。

为什么需要设计模式来管理定时任务?

先看一个反面教材。我曾经接手过一个老项目,定时任务代码长这样:

// 反面示例:所有逻辑揉在一起
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));
    }
}
注意:观察者的通知方法不要阻塞任务执行。我建议用异步方式通知,或者把观察者放到独立线程池里执行。否则一个慢的观察者会拖慢整个调度器。

整体架构图

下面这张图展示了三个模式如何协同工作:

定时任务系统架构:三种设计模式协同 命令模式 封装任务 TaskCommand 接口 CleanupTask EmailTask ReportTask ...其他任务 策略模式 选择调度策略 ScheduleStrategy 接口 FixedRateStrategy CronStrategy DelayStrategy ...其他策略 观察者模式 监控任务状态 TaskObserver 接口 LoggingObserver AlertObserver MetricsObserver ...其他观察者 TaskScheduler(调度器) 调用 使用 通知

调度器是核心,它持有任务对象(命令模式)、策略对象(策略模式),并通知观察者(观察者模式)。三者各司其职,互不干扰。

实战中的避坑指南

这套方案我用了好几年,踩过一些坑,分享给你:

  • 任务执行时间超过调度间隔怎么办? 我曾经遇到过清理任务跑了10分钟,但调度间隔是5分钟,结果任务堆积。解决方案:用FixedDelay代替FixedRate,或者加一个「是否正在执行」的标志位。
  • 观察者太多影响性能? 我建议用异步通知。把观察者的回调放到一个独立线程池里,或者用消息队列解耦。
  • 策略切换时任务状态丢失? 如果任务正在执行,突然切换策略,可能会出问题。我习惯在切换前先停止调度器,等当前任务执行完再重新启动。
  • 分布式环境下任务重复执行? 命令模式的任务对象最好实现幂等性。同一个任务执行两次,结果应该一样。
核心总结:命令模式让任务可管理,策略模式让调度可配置,观察者模式让状态可监控。三个模式组合起来,定时任务系统就变得清晰、灵活、可扩展。下次你写定时任务,不妨试试这个思路。

好了,今天就聊到这儿。这套组合拳你可以在自己的项目里试试,有什么问题欢迎交流。


公众号:蓝海资料掘金营,微信deep3321