揭秘xxl-job:从高可用到调度一致性

揭秘xxl-job:从高可用到调度一致性

在快节奏的互联网世界里,每一项业务都像一支高速运转的股票,需要精准的交易策略和风控系统来支撑。而我们的任务调度系统,就如同股市里的"基金经理",负责在正确的时点(收盘、清算、结算),执行正确的操作(发送账单、生成报表、数据同步),确保每一笔"交易"都精准无误。

想象一下,如果这位"基金经理"只有一个,一旦他离岗或身体不适,整个盘面就会陷入停滞,业务将面临巨大的风险。在技术世界里,这就是我们常说的单点故障(Single Point of Failure) 。为了防止"崩盘",我们需要构建一个由多位"基金经理"组成的团队,让他们相互备份、协同工作,确保无论何时都能稳健地管理好每一笔交易。

这,正是分布式调度系统的核心价值所在。我们将深入探讨其中的几个关键概念:分布式与高可用(HA)Admin 多节点部署调度一致性保障 以及执行器高可用机制。理解这些,是构建一个健壮、可靠的分布式调度系统的基石,确保你的业务"股票"持续健康地增长。

1. 分布式与高可用(HA):为什么不能单打独斗?

单机系统最大的风险在于单点故障(Single Point of Failure, SPOF) 。一旦这唯一的节点宕机,整个系统就会瘫痪。这就好比一个公司只靠一个总调度员来管理所有生产线,一旦他缺席,所有生产都会停滞。

为了解决这个问题,我们引入了分布式和**高可用(HA)**的设计理念。

  • 分布式:将系统的功能分散部署在多个节点上,让不同的机器承担不同的角色或相同的角色,从而避免所有鸡蛋放在一个篮子里的风险。
  • 高可用(HA) :通过冗余(Redundancy)来消除单点故障。当某个节点发生故障时,其他健康的节点能迅速接管其工作,确保服务不中断。

Admin 多节点部署和执行器高可用,正是这两种思想在调度系统中的具体实践。

2. Admin 多节点部署:打造"不倒翁"调度中心

Admin 节点是调度系统的"大脑",负责任务的调度、管理和监控。如果只有一个 Admin 节点,那它就是整个系统的最大风险点。

Admin 多节点部署 的核心原理在于,将多个 Admin 服务部署成一个集群。这些节点不依赖本地存储,而是共享一个中心化的数据库(通常是 MySQL)。

  • 如何实现高可用? 当一个 Admin 节点宕机后,其他节点可以继续从共享数据库中读取任务信息,并承担起调度工作,保证调度服务不会中断。
  • 如何实现数据同步? 所有 Admin 节点都读写同一个数据库。任务信息、执行日志、注册的执行器列表等所有关键数据都集中存储在此。这就像一个团队成员都共享同一个项目管理文档,确保信息一致。

在 XXL-JOB 的配置文件 application.properties 中,所有 Admin 节点都配置相同的数据库连接信息,指向同一个 MySQL 实例。

yml 复制代码
### database
spring.datasource.url=jdbc:mysql://db-host:3306/xxl-job?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=password

3. 调度一致性保障:如何避免"重复派单"?

当 Admin 部署了多个节点后,一个新的问题出现了:如何确保多个节点不会同时调度同一个任务?

如果一个每分钟执行一次的任务,在整点到来时,两个 Admin 节点都发现它需要被执行,并同时尝试调度,这会导致任务被重复触发,造成业务逻辑混乱。

为了解决这个问题,分布式调度系统引入了分布式锁 机制,确保在同一时间只有一个 Admin 节点能够对某个任务进行调度。在 XXL-JOB 中,这个锁是利用数据库的行级锁来实现的。

源码:XXL-JOB 的调度线程会定期扫描数据库中需要执行的任务。在每次扫描并处理之前,它会尝试获取一个数据库锁。只有成功获取锁的线程才能继续,否则会跳过本次调度,从而避免重复执行。

Java 复制代码
// JobScheduleHelper.java
public void start() {
    // ...
    // 调度线程循环
    while (!scheduleThreadToStop) {
        Connection conn = null;
        try {
            conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
            conn.setAutoCommit(false); // 开启事务

            // 核心锁逻辑:尝试对名为 'schedule_lock' 的记录加行级锁
            preparedStatement = conn.prepareStatement("select * from xxl_job_lock where lock_name = 'schedule_lock' for update");
            preparedStatement.execute();

            // 如果成功获取锁,继续执行调度任务的逻辑
            // ...

            conn.commit(); // 提交事务,释放锁
        } catch (Throwable e) {
            // ... 异常处理,回滚事务 ...
        } finally {
            // ... 释放资源 ...
        }
        // ...
    }
}
  • 加锁竞争 :当任务需要被调度时,所有 Admin 节点都会尝试执行一个 SELECT ... FOR UPDATE 语句来锁定调度资源。
  • 原子性保障:由于数据库锁的特性,只有一个 Admin 节点能成功获取锁。这个节点会继续执行后续的调度逻辑,其他未获取到锁的节点会放弃本次调度,从根本上解决了重复调度的问题。

4. 执行器高可用机制:任务执行的"B计划"

解决了 Admin 节点的单点故障和调度一致性后,我们还需要关注任务执行端。如果一个任务已经下发给了执行器,但执行器在执行过程中突然宕机了,该怎么办?

为了保障执行器的高可用性,系统通常会结合以下两种机制:

  1. 心跳机制 :Admin 节点会周期性地向所有已注册的执行器发送心跳请求。如果连续多次没有收到某个执行器的响应,Admin 就会认为该执行器已宕机
  2. 故障转移(Failover) :这是在发现执行器故障后的补救措施。一旦 Admin 发现某个正在执行任务的执行器宕机,它会立即将该任务标记为失败,并根据预设的策略(如重试次数),将任务重新调度到集群中另一个健康的执行器上。

执行器的注册与发现机制是实现这一点的基础。每个执行器启动时,都会向 Admin 报告自己的地址、负载等信息。Admin 维护着一个健康的执行器列表,当需要进行故障转移时,可以从这个列表中选择一个合适的"替补"来接管任务。

4.1 故障检测:谁来判断执行器是否存活?

故障检测是在 Admin 端完成的。Admin 端会有一个后台监控线程,通过定时扫描数据库来判断执行器的存活状态。

源码 :故障检测的逻辑主要在 com.xxl.job.admin.core.thread.JobRegistryHelper 类中。

Java 复制代码
// JobRegistryHelper.java
public void start() {
    registryMonitorThread = new Thread(() -> {
        while (!toStop) {
            try {
                // 1. 查询数据库中,心跳超时的"死亡"执行器
                List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao()
                    .findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
                
                // 2. 将超时的执行器从注册表中移除
                if (ids != null && ids.size() > 0) {
                    XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
                }
            } catch (Throwable e) {
                // ...
            }
            // 线程休眠,等待下一次检测
            try {
                TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
            } catch (Throwable e) {
                // ...
            }
        }
    });
    // ...
}

4.2 任务重新调度:失败或宕机后如何处理?

任务的重新调度同样是在 Admin 端 完成的,它会监控两种情况:任务执行失败执行器宕机导致的任务卡死

源码 :重新调度的核心逻辑位于 com.xxl.job.admin.core.thread.JobFailMonitorHelper 类中。

Java 复制代码
// JobFailMonitorHelper.java (简化版)
public void start() {
    monitorThread = new Thread(() -> {
        while (!toStop) {
            try {
                // 1. 查询数据库,找出所有执行失败的任务日志(包括主动上报失败和因宕机而卡死的任务)
                List<Long> failLogIds = xxlJobLogDao.findFailJobLogIds(1000);

                if (failLogIds != null && !failLogIds.isEmpty()) {
                    for (long failLogId : failLogIds) {
                        XxlJobLog log = xxlJobLogDao.load(failLogId);
                        
                        // 2. 如果任务配置了失败重试次数
                        if (log.getExecutorFailRetryCount() > 0) {
                            // 3. 核心:调用触发器,将任务重新调度
                            JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount() - 1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
                            
                            // 4. 更新日志,记录重试信息
                            log.setTriggerMsg(log.getTriggerMsg() + "<br><br><span style="color:#F39C12;" > >>>>>>>>>>> Retry Trigger... <<<<<<<<<<< </span><br>");
                            xxlJobLogDao.updateTriggerInfo(log);
                        }
                    }
                }
            } catch (Throwable e) {
                // ...
            }
            // 线程休眠,等待下一次扫描
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (Throwable e) {
                // ...
            }
        }
    });
    // ...
}

5. 执行器心跳:如何向 Admin 报告存活?

执行器端的心跳逻辑封装在一个独立的线程中,它会定期向 Admin 发送注册请求,从而实现"心跳续约"。

源码 :执行器心跳逻辑位于 com.xxl.job.core.thread.ExecutorRegistryThread

Java 复制代码
// ExecutorRegistryThread.java (简化版)
public void start(final String appname, final String address) {
    registryThread = new Thread(() -> {
        // 循环执行,直到停止
        while (!toStop) {
            try {
                // 构建注册/心跳参数
                RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                
                // 遍历所有 Admin 地址,发送注册/心跳请求
                for (AdminBiz adminBiz : XxlJobExecutor.getAdminBizList()) {
                    ReturnT<String> registryResult = adminBiz.registry(registryParam);
                    if (registryResult != null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                        // 只要有一个 Admin 接收成功,就跳出循环
                        break;
                    }
                }
            } catch (Throwable e) {
                // ...
            }
            
            // 睡眠,等待下一次心跳,默认30秒
            try {
                TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
            } catch (Throwable e) {
                // ...
            }
        }
        // ... 线程停止时,发送注销请求 ...
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
}

总结

构建一个健壮的分布式调度系统,不仅仅是将单机应用简单地扩展到多机,更需要深入思考和解决分布式环境下的种种挑战。通过 Admin 多节点部署来消除单点故障 ,利用数据库锁来保障调度一致性 ,以及通过心跳、故障检测和自动重试机制来保证执行器高可用,这些技术共同构筑了一个可靠、高效的任务调度体系,为业务的稳定运行保驾护航。

相关推荐
Moonbit2 小时前
MoonBit 三周年 | 用代码写就 AI 时代的语言答卷
后端·程序员·编程语言
菜鸟谢2 小时前
QEMU
后端
玉衡子2 小时前
六、深入理解JVM执行引擎
java·jvm
bobz9653 小时前
calico vxlan 默认不依赖 BGP EVPN 携带 VNI
后端
bobz9653 小时前
vxlan 和 vlan 的不同点
后端
每天进步一点_JL3 小时前
JVM 内存调优:到底在调什么?怎么调?
java·jvm·后端
程序员海军3 小时前
如何让AI真正理解你的需求
前端·后端·aigc
yinke小琪3 小时前
说说Java 中 Object 类的常用的几个方法?详细的讲解一下
java·后端·面试
回家路上绕了弯3 小时前
主从架构选型指南:从原理到落地,搞懂怎么选才适合你的业务
后端·架构