Agent Java项目入门指南:从零搭建高可用的分布式任务调度系统

1次阅读
没有评论

共计 2871 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

背景痛点:为什么需要分布式任务调度

在传统单机定时任务(如 Spring 自带的 @Scheduled)中,我们经常会遇到两个致命问题:

Agent Java 项目入门指南:从零搭建高可用的分布式任务调度系统

  • 单点故障 :如果部署定时任务的服务器宕机,所有任务都会停止运行

  • 任务重复执行 :在集群环境下,如果不加控制,同一个任务可能被多个节点同时触发

我曾在一个电商项目中遇到过惨痛教训:促销活动的库存同步任务因为单节点崩溃而停滞,导致前端页面显示库存与实际相差 30%。这促使我开始研究分布式任务调度方案。

技术选型:主流框架对比

特性 Quartz XXL-JOB Elastic-Job Agent 架构
分布式支持 弱 (需 DB 锁)
可视化界面 可扩展
任务分片 不支持 支持 支持 支持
故障转移 手动实现 自动 自动 自动 + 智能

选择 Agent 架构的关键原因:

  1. 与 Spring Boot 生态无缝集成
  2. 基于 ZooKeeper 的协调机制比数据库轮询更高效
  3. 灵活的扩展点设计,方便二次开发

核心实现三步走

1. 快速集成

在 Spring Boot 项目中只需两步:

  1. 添加 starter 依赖(注意版本号):
<dependency>
    <groupId>com.agent</groupId>
    <artifactId>agent-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
  1. 在 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

动态扩缩容策略

  1. 为每个任务组设置独立的线程池
  2. 使用 Kubernetes 的 HPA 自动扩缩容
  3. 通过 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;
}

进阶思考:补偿机制设计

当任务执行时间超过调度间隔时,我的解决方案是:

  1. 在任务表中记录本次执行的 startTime 和 endTime
  2. 下次调度时检查上次任务是否完成
  3. 如果未完成,根据业务场景选择:
  4. 等待完成(适合必须串行的任务)
  5. 跳过本次(适合可容忍丢失的任务)
  6. 强制终止并重新执行(适合幂等任务)

完整实现代码已放在 GitHub: 解决方案示例

踩坑经验分享

  1. ZK 连接不稳定 :务必配置合理的 sessionTimeout 和 retryPolicy
  2. 线程池阻塞 :避免在任务中使用同步 IO 操作
  3. 时间同步问题 :所有节点必须使用 NTP 保持时间同步
  4. 内存泄漏 :定期检查 ThreadLocal 变量的使用

通过这个项目,我最大的收获是理解了分布式系统中最核心的『状态一致性』问题。Agent 架构通过 ZK 的 Watcher 机制,用事件驱动的方式实现了简洁高效的集群协调,这种设计思路值得在其他分布式组件中借鉴。

正文完
 0
评论(没有评论)