共计 2871 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点:为什么需要分布式任务调度
在传统单机定时任务(如 Spring 自带的 @Scheduled)中,我们经常会遇到两个致命问题:

-
单点故障 :如果部署定时任务的服务器宕机,所有任务都会停止运行
-
任务重复执行 :在集群环境下,如果不加控制,同一个任务可能被多个节点同时触发
我曾在一个电商项目中遇到过惨痛教训:促销活动的库存同步任务因为单节点崩溃而停滞,导致前端页面显示库存与实际相差 30%。这促使我开始研究分布式任务调度方案。
技术选型:主流框架对比
| 特性 | Quartz | XXL-JOB | Elastic-Job | Agent 架构 |
|---|---|---|---|---|
| 分布式支持 | 弱 (需 DB 锁) | 强 | 强 | 强 |
| 可视化界面 | 无 | 有 | 有 | 可扩展 |
| 任务分片 | 不支持 | 支持 | 支持 | 支持 |
| 故障转移 | 手动实现 | 自动 | 自动 | 自动 + 智能 |
选择 Agent 架构的关键原因:
- 与 Spring Boot 生态无缝集成
- 基于 ZooKeeper 的协调机制比数据库轮询更高效
- 灵活的扩展点设计,方便二次开发
核心实现三步走
1. 快速集成
在 Spring Boot 项目中只需两步:
- 添加 starter 依赖(注意版本号):
<dependency>
<groupId>com.agent</groupId>
<artifactId>agent-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
- 在 application.yml 中配置 ZooKeeper 地址:
agent:
zookeeper:
servers: 192.168.1.100:2181,192.168.1.101:2181
namespace: /agent-cluster
2. 解剖三大核心组件
任务注册中心
@Slf4j
public class JobRegistry {
// 使用 CuratorFramework 与 ZK 交互
@Autowired
private CuratorFramework client;
/**
* 注册任务到 ZK 持久节点
* @throws Exception 当 ZK 连接异常时抛出
* @see org.apache.curator.framework.api.CuratorEvent
*/
public void register(JobDefinition job) throws Exception {String path = "/jobs/" + job.getJobId();
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath(path, job.toString().getBytes());
}
}
调度引擎
关键点在于使用 ZK 的 Watcher 机制实现事件监听:
// 监听任务节点变化
public void watchJobNodes() {PathChildrenCache cache = new PathChildrenCache(client, "/jobs", true);
cache.getListenable().addListener((client, event) -> {switch (event.getType()) {
case CHILD_ADDED:
scheduleJob(event.getData());
break;
case CHILD_REMOVED:
cancelJob(event.getData());
break;
}
});
}
执行器设计
建议采用线程池隔离不同优先级的任务:
@Bean
public Executor jobExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("job-exec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
3. 集群协调关键代码
故障转移的核心逻辑:
// 在 ZK 上创建临时节点表示活跃节点
String leaderPath = "/leader";
client.create().withMode(CreateMode.EPHEMERAL)
.forPath(leaderPath, instanceId.getBytes());
// 设置监听
client.getData().usingWatcher((Watcher) event -> {if (event.getType() == EventType.NodeDeleted) {electNewLeader(); // 重新选举
}
}).forPath(leaderPath);
生产环境必备配置
日志存储方案对比
| 方案 | 查询性能 | 存储成本 | 实时性 | 适合场景 |
|---|---|---|---|---|
| ELK | 高 | 高 | 秒级 | 需要复杂日志分析 |
| Grafana | 中 | 中 | 分钟级 | 监控告警为主 |
建议:关键业务日志用 ELK,性能指标用 Grafana
动态扩缩容策略
- 为每个任务组设置独立的线程池
- 使用 Kubernetes 的 HPA 自动扩缩容
- 通过 ZK 的临时节点感知节点变化
JMX 监控配置
@Bean
public MBeanServer mBeanServer() {MBeanServerFactoryBean factory = new MBeanServerFactoryBean();
factory.setLocateExistingServerIfPossible(true);
return factory.getObject();}
@Bean
public Exporter exporter() {AnnotationMBeanExporter exporter = new AnnotationMBeanExporter();
exporter.setServer(mBeanServer());
return exporter;
}
进阶思考:补偿机制设计
当任务执行时间超过调度间隔时,我的解决方案是:
- 在任务表中记录本次执行的 startTime 和 endTime
- 下次调度时检查上次任务是否完成
- 如果未完成,根据业务场景选择:
- 等待完成(适合必须串行的任务)
- 跳过本次(适合可容忍丢失的任务)
- 强制终止并重新执行(适合幂等任务)
完整实现代码已放在 GitHub: 解决方案示例
踩坑经验分享
- ZK 连接不稳定 :务必配置合理的 sessionTimeout 和 retryPolicy
- 线程池阻塞 :避免在任务中使用同步 IO 操作
- 时间同步问题 :所有节点必须使用 NTP 保持时间同步
- 内存泄漏 :定期检查 ThreadLocal 变量的使用
通过这个项目,我最大的收获是理解了分布式系统中最核心的『状态一致性』问题。Agent 架构通过 ZK 的 Watcher 机制,用事件驱动的方式实现了简洁高效的集群协调,这种设计思路值得在其他分布式组件中借鉴。
正文完
