调度器模式:任务接口、调度策略与执行引擎
调度器模式,说白了就是「谁在什么时候做什么事」。我在做中间件开发那几年,几乎每个系统都离不开它——定时报表、数据同步、批处理任务……你想想看,如果没有一套统一的调度机制,这些散落在代码各处的定时逻辑,迟早会变成一团乱麻。
今天我就带你拆解一下调度器模式的核心结构。它其实就三块:任务接口、调度策略、执行引擎。搞懂了这三者的关系,你就能自己搭一个轻量级的调度框架。
一、任务接口:定义「做什么」
任务接口是调度器的基础。它规定了每个任务必须暴露什么行为。我个人习惯把任务接口设计得尽量薄,只保留最核心的方法。
// Java 版本
public interface Task {
String getTaskId();
void execute(TaskContext context);
}
// C++ 版本
class Task {
public:
virtual ~Task() = default;
virtual std::string getTaskId() const = 0;
virtual void execute(const TaskContext& context) = 0;
};
嗯,这里要注意:TaskContext 里放什么?我建议放执行时间、重试次数、全局配置这些元信息。别把业务数据塞进去,否则接口就脏了。
二、调度策略:决定「什么时候做」
调度策略是调度器的灵魂。它决定了任务的触发时机。常见的策略有:
- 固定延迟:上次执行完后,等固定时间再执行下一次
- 固定频率:不管上次执行多久,按固定间隔触发
- Cron 表达式:精确到秒级的定时触发
- 一次性延迟:指定一个未来时间点执行一次
我建议把调度策略抽象成接口,这样扩展起来非常方便。
// Java 版本
public interface ScheduleStrategy {
long nextExecutionTime(long currentTime, TaskContext context);
}
// Cron 策略实现
public class CronScheduleStrategy implements ScheduleStrategy {
private final CronExpression cron;
@Override
public long nextExecutionTime(long currentTime, TaskContext context) {
return cron.nextTimeAfter(currentTime);
}
}
三、执行引擎:负责「怎么执行」
执行引擎是调度器的核心。它把任务和策略组合起来,管理线程池、处理异常、记录日志。我见过不少团队把调度逻辑散写在业务代码里,结果出了问题根本没法排查。
一个健壮的执行引擎至少要做这几件事:
- 任务注册:把任务和它的调度策略绑定
- 时间轮询:不断检查哪些任务到了执行时间
- 线程池管理:控制并发度,防止资源耗尽
- 异常处理:任务挂了不能影响其他任务
- 状态监控:记录每次执行的开始、结束、耗时
// Java 版本 - 简化版执行引擎
public class SchedulerEngine {
private final ScheduledExecutorService executor;
private final Map<String, ScheduledFuture<?>> tasks = new ConcurrentHashMap<>();
public void register(Task task, ScheduleStrategy strategy) {
Runnable wrapper = () -> {
try {
task.execute(new TaskContext());
} catch (Exception e) {
// 记录异常,但不中断调度循环
log.error("Task {} failed", task.getTaskId(), e);
}
};
ScheduledFuture<?> future = executor.schedule(wrapper,
strategy.nextExecutionTime(0, null), TimeUnit.MILLISECONDS);
tasks.put(task.getTaskId(), future);
}
}
四、定时任务与批处理框架
说到定时任务,很多人第一反应是 Quartz、XXL-JOB 这些框架。但我想说,理解调度器模式比会用某个框架重要得多。框架会变,模式不会。
批处理框架其实也是调度器模式的一种特化。它多了几个东西:
- 分片策略:把大任务拆成小片并行处理
- 失败重试:某一片失败了,只重试那一片
- 进度追踪:记录处理了多少条,还剩多少条
我参与过一个日处理千万级数据的批处理系统,就是用调度器模式搭的骨架。任务接口定义数据读取、处理、写入三个步骤,调度策略用固定频率(每 5 分钟拉一次),执行引擎用线程池控制并发。结构清晰,出了问题也容易定位。
五、知识体系图
下面这张图展示了调度器模式的整体结构。你可以看到任务、策略、引擎三者如何协作。
六、实战中的取舍
最后聊点实际的。你在设计调度器时,有几个问题要想清楚:
| 问题 | 我的建议 |
|---|---|
| 单机还是分布式? | 单机用 ScheduledExecutorService 就够了。分布式需要引入分布式锁或选主机制。 |
| 任务持久化? | 如果任务丢了会出大事,就存数据库。否则内存里跑着就行。 |
| 任务超时怎么处理? | 我习惯给每个任务设一个超时阈值,超时了就强制中断,并记录告警。 |
| 要不要支持动态注册? | 如果业务经常变,就做成动态的。否则启动时一次性注册更简单。 |
调度器模式看起来简单,但想做得健壮并不容易。我见过太多「临时写个定时任务」最后变成线上事故的例子。把任务、策略、引擎这三个角色分清楚,你的调度系统就能经得起折腾。
公众号:蓝海资料掘金营,微信deep3321