跳到正文

XXL-JOB 架构总览与核心概念 ​

一句话:XXL-JOB 把「什么时候调度」和「在哪里执行」拆成两个进程——Admin 管调度,Executor 管执行,MySQL 同时充当配置库、注册表和分布式锁;换来的是部署简单,代价是 Admin 与 DB 成为中心。

适用版本:XXL-JOB 2.3.x / 2.4.0。默认 schema 是 8 张表,不是 6 张。

为什么需要 ​

单机 @Scheduled 把调度规则和业务代码绑在同一个 JVM 里:进程挂了任务全停;一亿条数据只能一台机器跑,多部署一份又会重复执行;任务散在 N 台机器上,改 cron 要登机器,也看不到历史结果。

调度框架的本质,是把「什么时候叫谁干什么」抽出来交给独立控制面。XXL-JOB 选的是中心化控制面 + 无状态执行器:Admin 做决策,Executor 只负责跑 Handler 并回报结果。

核心机制 ​

三件套 ​

Admin:JobScheduleHelper 扫表认领 → JobTriggerPoolHelper 发 /run
       JobRegistryHelper 维护注册表;/api/registry、/api/callback 收心跳和结果
Executor:XxlJobSpringExecutor 启动内嵌 HTTP Server
          JobThread 每个 jobId 一个线程;ExecutorRegistryThread 每 30s 心跳
          TriggerCallbackThread 把结果回调给 Admin
MySQL:xxl_job 库的 8 张表,配置、日志、注册、锁、用户

调度不是 Quartz Scheduler 触发的 ​

2.x 的 Admin 里没有 Quartz Scheduler、没有 Trigger/JobDetail,也没有 Quartz 的 misfire 机制。它只保留了一份从 Quartz 移植的 CronExpression(com.xxl.job.admin.core.cron),做两件事:解析 cron、算下一次触发时间。

真正的触发由 JobScheduleHelper 完成:

  1. 调度线程在事务里执行 SELECT * FROM xxl_job_lock WHERE lock_name='schedule_lock' FOR UPDATE。
  2. 查 trigger_status=1 AND trigger_next_time <= now + 5s 的任务(5 秒预读)。
  3. 到期的立刻提交给触发线程池;未来 5 秒内的放进 60 槽秒级时间环。
  4. 时间环线程每秒检查当前秒(并回看前 2 秒),把到点的 jobId 提交给线程池。
  5. HTTP 调用在行锁事务之外,锁只覆盖「扫表 + 认领」这一小段。

所以 Admin 的 HA 不靠选主,靠这行记录的行锁:多个 Admin 同时扫,只有一个能进入临界区;另一个阻塞,等它提交后看到已经推进的 trigger_next_time。

xxl_job_lock 是行锁,没有 30s 租约 ​

xxl_job_lock 只有一行 schedule_lock。行锁由持有它的事务释放,不是由字段的过期时间释放。Admin 崩溃时连接断开、事务回滚,锁自动释放;没有「30 秒租约到期」。

所以不要用 UPDATE xxl_job_lock 去「释放锁」:如果另一个会话正持有 FOR UPDATE,这条 UPDATE 只会一起阻塞,等到 innodb_lock_wait_timeout(MySQL 默认 50s)后报错。正确处置见 07-生产踩坑与运维兜底。

8 张表 ​

表作用关键点
xxl_job_info任务定义trigger_status(0 停/1 跑)、trigger_next_time、cron、handler、路由/阻塞策略
xxl_job_log每次触发一条trigger_code 是调度结果,handle_code 是执行结果,handle_time 是结束时间
xxl_job_log_report按天汇总运行中/成功/失败数量
xxl_job_logglueGLUE 代码版本支持回滚,生产更常用 BEAN
xxl_job_group执行器组app_name、address_type(0 自动/1 手动)、address_list
xxl_job_registry心跳行(registry_group, registry_key, registry_value) + update_time
xxl_job_user登录用户默认 admin/123456,上线必须改
xxl_job_lock调度行锁只有一行 schedule_lock,不是业务锁

默认建表脚本 doc/db/tables_xxl_job.sql 没有分区,日志清理走 DELETE 分批删除(默认保留 30 天)。「按 trigger_time 分区 + DROP PARTITION」要自己改表结构,不是默认能力。

最小上手 ​

bash
# 环境:JDK 8+、MySQL 5.7/8.0、Maven 3.6+;以 2.4.0 为例
git clone --depth 1 --branch v2.4.0 https://github.com/xuxueli/xxl-job.git
mysql -uroot -p < xxl-job/doc/db/tables_xxl_job.sql
mysql -uroot -p -e "use xxl_job; show tables;"   # 应看到 8 张表

Admin 的 application.properties 至少改数据源,然后启动。默认登录 admin/123456:

properties
server.port=8080
server.servlet.context-path=/xxl-job-admin
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
xxl.job.accessToken=default_token
bash
cd xxl-job && mvn -pl xxl-job-admin -am -DskipTests package
java -jar xxl-job-admin/target/xxl-job-admin-2.4.0.jar

Executor 侧加 com.xuxueli:xxl-job-core:2.4.0,配好地址后声明 Bean:

properties
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
xxl.job.accessToken=default_token
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.port=9999
java
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
    executor.setAdminAddresses("http://127.0.0.1:8080/xxl-job-admin");
    executor.setAppname("xxl-job-executor-sample");
    executor.setPort(9999);
    executor.setAccessToken("default_token");
    return executor;
}

@Component
public class DemoJob {
    @XxlJob("demoJobHandler")
    public void demoJobHandler() {
        XxlJobHelper.log("param={}", XxlJobHelper.getJobParam());
        XxlJobHelper.handleSuccess("ok");
    }
}

启动后,「执行器管理」应看到 xxl-job-executor-sample 的在线地址;新建任务,Cron 填 0/30 * * * * ?,JobHandler 填 demoJobHandler,点「执行一次」,日志详情应看到成功记录。

生产动作 ​

配置/常量默认值(2.4.0)说明
xxl.job.accessToken空生产必须设置,两端一致
xxl.job.executor.port9999内嵌 HTTP Server 端口
xxl.job.triggerpool.fast.max200快线程池最大线程数
xxl.job.triggerpool.slow.max100慢线程池最大线程数
RegistryConfig.BEAT_TIMEOUT30心跳间隔(秒)
RegistryConfig.DEAD_TIMEOUT90注册表过期阈值(秒)

排障先看这四处:

  1. 「执行器地址为空」→ 查 xxl_job_group.app_name 与 xxl.job.executor.appname 是否一致;address_type=0 时再看 xxl_job_registry 有没有行。
  2. /run 连接超时 → 从 Admin 机器 curl http://executor-ip:9999/beat;容器自动取到的可能是内网 IP,需显式配 xxl.job.executor.ip 或 address。
  3. 任务不触发 → 查 trigger_status 是否为 1、trigger_next_time、Admin 与 DB 时区。
  4. 登录 401/回调被拒 → 两端 accessToken 不一致,或请求没带 XXL-JOB-ACCESS-TOKEN 头。

边界 ​

  • 不是延迟队列:cron 最低 1 秒,且有扫表/线程池抖动;「3 秒后发提醒」用消息队列。
  • 不是工作流引擎:childJobId 只是简单串联,没有分支、汇聚、补偿。
  • 不保证恰好一次:调度、HTTP、回调都可能重复或丢失,业务必须用业务键幂等。
  • 中心化有代价:Admin 全挂时新任务不再触发(已在 Executor 上跑的不受影响);MySQL 挂了整体不可用。
  • 注册中心不是 ZK:只是 Admin 维护的一张地址表,没有 Watch、没有强一致选举。

讲清楚 ​

XXL-JOB 可以记成三句话:Admin 决定「什么时候、让谁跑」,Executor 只负责「把 Handler 跑起来」,MySQL 是两者共同的记账本。Admin 里没有 Quartz 调度器,只有一份 CronExpression 算下次时间;真正触发靠 JobScheduleHelper 扫 xxl_job_info,5 秒预读加 60 槽秒级时间环,多个 Admin 用 xxl_job_lock 的行锁串行化,锁由事务持有,没有租约。Executor 每 30 秒注册一次,Admin 每 30 秒清理超过 90 秒没心跳的地址。调度是 /run,结果是 /callback,都是 HTTP,所以 Admin 发完请求就能释放连接。代价是中心化和 MySQL 依赖,收益是部署和排障足够简单。

适用 XXL-JOB 2.3.x / 2.4.0 | 初稿 2026-10

把一门技术讲透到既懂也会用