XXL-JOB
XXL-JOB 是一个中心化的分布式任务调度平台:调度中心(admin)负责「什么时候该跑」,执行器(executor)负责「跑什么」,两侧靠一条极薄的 HTTP 协议连接。它的价值不在功能多,而在于用最少的机制把「不重不漏」这件事做到生产可用——一张 DB 行锁表 + 一个 60 刻度的秒级时间轮,就替换掉了整个 Quartz。
本册问题地图
Section titled “本册问题地图”XXL-JOB 的核心不是 Cron 表达式,而是把一次任务拆成“计划、路由、执行、回调、重试和留痕”六个阶段。本册围绕一次调度实例回答:
- 谁决定任务现在应该触发? 调度中心扫描近期开启任务,把近秒触发放入时间轮。
- 有多个执行器时交给谁? 注册表给出候选节点,路由策略结合任务语义选择目标。
- 同一任务尚未结束又被触发怎么办?
JobThread与阻塞策略定义串行、丢弃或替换语义。 - 失败之后为什么不能只“再试一次”? 重试会创建新的调度压力,非幂等任务还可能产生重复副作用。
- 执行发生在远端,调度中心如何知道结果? 执行器保存日志并回调结果,admin 负责完成状态、告警和后续重试。
- 分片广播和负载均衡有什么区别? 前者要求多个节点共同覆盖数据,后者通常只选择一个节点执行。
读完整册后,应当能画出一条从 schedule_time 到执行结果落库的闭环,并指出每个阶段失败时由谁补偿。
| 项 | 值 |
|---|---|
| 仓库 | xuxueli/xxl-job |
| 本地路径 | E:\source\java\rpc\xxl-job |
| 分支 | master |
| Commit | e74c784f(2026-07-22) |
| 最近 tag | 3.4.2(pom.xml 里 version 为 3.5.0-SNAPSHOT) |
| 构建 | Maven 多模块,maven.compiler.source=17 |
| 关键依赖 | Spring Boot 4.1.0 · Spring 7.0.8 · Netty 4.2.15.Final · xxl-tool 2.5.0 · xxl-sso 2.4.0 · Gson 2.14.0 |
本册所有
文件:行号坐标均基于上述 commit。行号会随上游演进漂移,类名与方法名相对稳定。
⚠️ 这一版和网上流传的 2.x 解析文章差异很大,读之前先对齐这五处改名,否则按老文章去源码里找类会全部落空:
| 维度 | 2.x(大多数文章写的) | 3.4.x(本册) |
|---|---|---|
| admin 包路径 | com.xxl.job.admin.core.* |
com.xxl.job.admin.business.scheduler.* |
| 全局配置单例 | XxlJobAdminConfig |
XxlJobAdminBootstrap(InitializingBean + DisposableBean) |
| 通信层 | 自研 XxlJobRemotingUtil 手写 HTTP |
xxl-tool 的 HttpTool.createClient()...proxy(Interface.class) 动态代理,老实现整体挪进 util/deprecated/ |
| 返回值 | ReturnT<T>(code/msg/content) |
Response<T>(isSuccess()/getData()),ReturnT 同样进了 deprecated |
| 后台登录 | 自研 cookie 登录 | 接入 xxl-sso |
util/deprecated/ 这个包本身就是一份很好的架构演进考古现场:XxlJobRemotingUtil / ReturnT / IpUtil / GsonTool / ShardingUtil 全在里面,能直接对比「自己写」和「抽成公共库」两个版本。
它解决什么问题
Section titled “它解决什么问题”XXL-JOB 的起点是**「Quartz 不好用」**,官方文档第 5.4.1 节把理由写得很直白:
| Quartz 的问题 | 具体代价 | XXL-JOB 的对策 |
|---|---|---|
任务需要持久化成 QuartzJobBean 落到 11 张表里 |
系统侵入性极强,改任务要动数据库 | 自研调度组件,只保留 8 张业务表,任务是普通 Spring Bean 上的一个 @XxlJob 方法 |
| 调度逻辑和业务代码耦合在同一进程 | 任务变重时调度器本身被拖慢 | 调度中心与执行器彻底进程分离,调度中心只发 HTTP,不跑业务 |
| 集群下「抢占式」拿 DB 锁,抢到的节点独自跑全部任务 | 节点负载悬殊 | 抢锁只用于分配触发时刻,真正的执行由执行器集群「协同分配式」承担 |
| 通过 API 管理任务,无可视化 | 改 cron 要发版 | Web 控制台 + OpenAPI |
该决策的原文出自 v2.1.0 Release Notes(2019-07-07):「自研调度组件,移除 quartz 依赖:一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性;触发:单节点周期性触发,运行事件如 delayqueue;调度:集群竞争,负载方式协同处理,锁竞争 - 更新触发信息 - 推送时间轮 - 锁释放 - 锁竞争」。最后那一串正是
JobScheduleHelper#scheduleThread至今的循环骨架。
Maven 三模块,xxl-job-core 被 admin 和执行器双向复用——这是理解整个项目的关键:它同时装着「调度中心要调用执行器的接口」和「执行器要调用调度中心的接口」,两边各实现一半、各代理一半。
| 模块 | 职责 | 关键包 |
|---|---|---|
xxl-job-core |
双向协议契约 + 执行器全部运行时 | openapi.executor(executor 侧接口) openapi.admin(admin 侧接口) thread server handler context log |
xxl-job-admin |
调度中心:Web 控制台 + 6 条后台线程 + 触发链路 | business.scheduler.thread business.scheduler.trigger business.scheduler.route business.scheduler.type business.scheduler.misfire business.scheduler.complete |
xxl-job-executor-samples |
三个接入样例:frameless(裸 main)· springboot · springboot-ai | — |
core 里的 openapi 包是整个双向通信的唯一契约面,只有两个接口共 11 个方法:
| 接口 | 方向 | 方法 | 实现方 | 代理方 |
|---|---|---|---|---|
ExecutorBiz |
admin → executor | beat idleBeat trigger kill log |
ExecutorBizImpl(执行器) |
XxlJobAdminBootstrap#getExecutorBiz |
AdminBiz |
executor → admin | callback registry registryRemove |
AdminBizImpl(调度中心) |
XxlJobExecutor#initAdminBizList |
AdminJobBiz |
外部系统 → admin | addJob updateJob removeJob startJob stopJob triggerJob |
AdminJobBizImpl |
OpenAPI 调用方自建 |
两侧都用同一句话生成客户端——HttpTool.createClient().url(...).header(token).proxy(接口.class)(XxlJobAdminBootstrap.java:162 与 XxlJobExecutor.java:241),服务端则各自用一个 switch(uri) 分发(EmbedServer.java:207 与 OpenApiController.java:69)。没有 RPC 框架,没有序列化协议协商,就是 POST + JSON + 手写路由表。
调度状态全部落在 8 张 MySQL 表上,其中真正参与调度热路径的只有 4 张:
| 表 | 作用 | 热路径角色 |
|---|---|---|
xxl_job_lock |
只有一行 schedule_lock,PRIMARY KEY(lock_name) |
调度线程每轮 SELECT ... FOR UPDATE 抢它 |
xxl_job_info |
任务定义 + trigger_next_time / trigger_last_time / trigger_status |
预读扫描与批量回写的对象 |
xxl_job_registry |
执行器心跳,唯一键 (registry_group, registry_key, registry_value) |
30s 心跳 upsert,90s 判死 |
xxl_job_log |
每次触发一行,trigger_code / handle_code 两段状态 |
触发前先 insert 拿 logId,回调时 update |
xxl_job_log的两段式状态是理解全局的钥匙:trigger_code记「调度中心有没有把请求发出去」,handle_code记「执行器有没有跑完并回调」。失败重试看前者,结果丢失监控看后者,两个哨兵线程各扫各的。
读码入口顺序
Section titled “读码入口顺序”不要从 Web 控制台读,那是外壳;从**「一个任务是怎么从数据库变成一次 HTTP 调用」**这条线读:
XxlJobAdminBootstrap#doStart(XxlJobAdminBootstrap.java:83-109) —— 6 个 Helper 的启动顺序就是依赖顺序,注释里明确标了depend on JobTriggerPoolHelper。JobScheduleHelper#start(JobScheduleHelper.java:45-257) —— 全项目最核心的 200 行,两个线程(scheduleThread / ringThread)+ 三个分支判定,读懂它 xxl-job 就懂了一半。JobTrigger#trigger→#processTrigger(JobTrigger.java:63/:134) —— 单次触发的完整装配:分片扇出、写日志拿 id、路由选址、发请求、回写结果。ExecutorBizImpl#trigger(ExecutorBizImpl.java:49-160) —— 执行器侧的入口,阻塞策略与 handler 换绑都在这一个方法里。JobThread#run(JobThread.java:89-246) —— 每个 jobId 一个常驻线程 + 一条LinkedBlockingQueue,串行语义的实现处。TriggerCallbackThreadHelper+JobCompleteHelper—— 结果怎么回来、回不来怎么办(磁盘重试 + 10 分钟丢失兜底)。- 最后再读
route/strategy/九个策略类 —— 每个都只有 20~100 行,是很好的「策略模式 + 有状态负载均衡」样本。
配合读:xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/jobhandler/ 下的样例 handler 是最准确的使用说明书;doc/db/tables_xxl_job.sql 是数据模型说明书。
- 整体架构:admin/executor 双进程分层、6 + 4 条后台线程全景、启动与调度两条主流程、扩展点
- DB 锁与秒级时间轮:
FOR UPDATE抢锁、5 秒预读、60 刻度环、三分支判定、批量合并更新、不重不漏的四道防线 - 执行器注册与路由策略:30/90s 心跳判死、
ON DUPLICATE KEYupsert、九种路由的有状态与无状态之分、一致性哈希的 100 虚拟节点 - 阻塞策略与失败重试:
JobThread单线程队列模型、三种阻塞策略、超时FutureTask的真相、重试链与告警的 CAS 加锁 - 日志回捞与分片广播:日志按
logId落盘、fromLineNum增量拉取、回调三级降级、分片广播的扇出与重试退化 - 面试专题:高频题、高难追问、场景题与源码级加分答法
| 对象 | 对照点 |
|---|---|
| Quartz | 同为 Java 调度框架。Quartz 抢锁后独自执行、任务落表;XXL-JOB 抢锁只为分配时刻,执行下沉到执行器集群 |
Netty HashedWheelTimer |
同为时间轮。Netty 是多层轮 + tick 精度可调的通用定时器;XXL-JOB 是单层 60 槽、刻度写死 1 秒的极简版,因为它只需要秒级精度 |
| Elastic-Job | 同为分布式调度。Elastic-Job 用 ZooKeeper 做协调与分片,XXL-JOB 用 MySQL 行锁,少一个中间件但多一个 DB 热点 |
| PowerJob | 后来者,Worker 侧支持 MapReduce 式子任务;XXL-JOB 的分片广播是它的简化版 |
| Nacos 注册中心 | 同为「心跳续约 + 超时摘除」。Nacos 是内存注册表 + Distro 协议同步;XXL-JOB 直接把注册表放在 MySQL,靠唯一索引做 upsert |
Related
Section titled “Related”- 整体架构:先看这篇建立全局视图
- RPC 与微服务:本分组索引
- Disruptor:同样用「预分配 + 序号」思路,对照进程内队列与跨进程调度的差异
- Java 微服务架构治理实践:站内已有的治理层笔记