主题
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 完成:
- 调度线程在事务里执行
SELECT * FROM xxl_job_lock WHERE lock_name='schedule_lock' FOR UPDATE。 - 查
trigger_status=1 AND trigger_next_time <= now + 5s的任务(5 秒预读)。 - 到期的立刻提交给触发线程池;未来 5 秒内的放进 60 槽秒级时间环。
- 时间环线程每秒检查当前秒(并回看前 2 秒),把到点的 jobId 提交给线程池。
- 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_logglue | GLUE 代码版本 | 支持回滚,生产更常用 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_tokenbash
cd xxl-job && mvn -pl xxl-job-admin -am -DskipTests package
java -jar xxl-job-admin/target/xxl-job-admin-2.4.0.jarExecutor 侧加 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=9999java
@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.port | 9999 | 内嵌 HTTP Server 端口 |
xxl.job.triggerpool.fast.max | 200 | 快线程池最大线程数 |
xxl.job.triggerpool.slow.max | 100 | 慢线程池最大线程数 |
RegistryConfig.BEAT_TIMEOUT | 30 | 心跳间隔(秒) |
RegistryConfig.DEAD_TIMEOUT | 90 | 注册表过期阈值(秒) |
排障先看这四处:
- 「执行器地址为空」→ 查
xxl_job_group.app_name与xxl.job.executor.appname是否一致;address_type=0时再看xxl_job_registry有没有行。 /run连接超时 → 从 Admin 机器curl http://executor-ip:9999/beat;容器自动取到的可能是内网 IP,需显式配xxl.job.executor.ip或address。- 任务不触发 → 查
trigger_status是否为 1、trigger_next_time、Admin 与 DB 时区。 - 登录 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