基于:XXL-Job 3.4.x(JDK17)/ 2.4.x(JDK8)、Apache ShardingSphere ElasticJob 3.0.5、Spring Boot 2.x / 3.x
深入解析微服务场景下分布式任务调度的两大主流框架:中心化架构的 XXL-Job 与 去中心化架构的 ElasticJob。覆盖核心概念与方案定位、双方设计思想与核心机制(调度线程模型、路由策略、分片广播、弹性伸缩等)、一次调度的完整链路、核心类关系图,以及选型建议与常见问题,"业务上了 Spring Cloud 之后定时任务怎么管"。
目录
- [核心理论与方案定位 ★★★](#核心理论与方案定位 ★★★)
- [1.1 单体定时任务的四个痛点](#1.1 单体定时任务的四个痛点)
- [1.2 分布式任务调度的核心概念](#1.2 分布式任务调度的核心概念)
- [1.3 主流方案定位表](#1.3 主流方案定位表)
- [1.4 中心化 vs 去中心化两大流派](#1.4 中心化 vs 去中心化两大流派)
- [XXL-Job 设计思想与核心机制 ★★★](#XXL-Job 设计思想与核心机制 ★★★)
- [2.1 中心化架构与通信模型](#2.1 中心化架构与通信模型)
- [2.2 调度线程模型与快慢线程池](#2.2 调度线程模型与快慢线程池)
- [2.3 任务执行线程模型与阻塞处理策略](#2.3 任务执行线程模型与阻塞处理策略)
- [2.4 十种路由策略全景](#2.4 十种路由策略全景)
- [2.5 分片广播机制](#2.5 分片广播机制)
- [2.6 失败重试、调度过期与告警](#2.6 失败重试、调度过期与告警)
- [2.7 GLUE 动态任务](#2.7 GLUE 动态任务)
- [ElasticJob 设计思想与核心机制 ★★★](#ElasticJob 设计思想与核心机制 ★★★)
- [3.1 去中心化与 ZooKeeper 协调](#3.1 去中心化与 ZooKeeper 协调)
- [3.2 分片与弹性伸缩](#3.2 分片与弹性伸缩)
- [3.3 作业类型与事件追踪](#3.3 作业类型与事件追踪)
- [主线流程剖析 ★★★](#主线流程剖析 ★★★)
- [4.1 XXL-Job 一次调度的完整链路](#4.1 XXL-Job 一次调度的完整链路)
- [4.2 ElasticJob 一次执行的完整链路](#4.2 ElasticJob 一次执行的完整链路)
- [4.3 两大框架链路对比](#4.3 两大框架链路对比)
- 核心类关系图
- [5.1 XXL-Job 核心类关系](#5.1 XXL-Job 核心类关系)
- [5.2 ElasticJob 核心类关系](#5.2 ElasticJob 核心类关系)
- 扩展点与常见问题
- [6.1 扩展点](#6.1 扩展点)
- [6.2 常见问题 FAQ](#6.2 常见问题 FAQ)
- [6.3 选型速查](#6.3 选型速查)
- 参考资料
1. 核心理论与方案定位 ★★★
1.1 单体定时任务的四个痛点
Spring Cloud 微服务化之后,定时任务如果还是"每个服务里写一个 @Scheduled",会立刻暴露四个问题:
| 痛点 | 表现 | 后果 |
|---|---|---|
| 单点故障 ❌ | 任务只跑在一台机器上,机器宕机任务停摆 | 凌晨批处理静默失败,第二天才发现 |
| 性能瓶颈 ❌ | 大数据量任务单机跑不完,无法水平扩容 | 跑批时间越来越长,挤压业务时段 |
| 集群重复执行 ❌ | 多实例部署时每个实例都会触发,加锁又易漏易重 | 重复扣款 / 重复发消息 |
| 无统一管控 ❌ | cron 写死在代码里,改周期要发版;无日志、无告警 | 不知道任务到底跑没跑、跑得怎么样 |
关键认知 :分布式任务调度的本质是把"什么时候跑 (调度)"和"在哪台机器上跑(执行)"解耦,并引入一个可靠的协调者解决"谁去执行、执行几次、失败了怎么办"三个分布式难题。
1.2 分布式任务调度的核心概念
两大框架术语不同但概念一一对应:
| 概念 | 说明 | XXL-Job 对应 | ElasticJob 对应 |
|---|---|---|---|
| 调度中心/注册中心 | 触发调度、保存任务配置、协调各节点的枢纽 | xxl-job-admin(独立部署的 Web 应用) |
ZooKeeper 注册中心 |
| 执行器/作业实例 | 真正干活的应用节点 | Executor(以依赖形式内嵌业务服务) | 每个应用节点,同时是调度器和执行器 |
| 任务/作业 | 一个调度单元 | Job(BEAN / GLUE 两种模式) | Job(Simple / Dataflow / Script / Http) |
| 分片 | 把一个任务的数据切成 N 片并行执行 | 分片广播(注入 index/total,业务自行切片) |
原生分片(框架分配 shardingItem) |
| 路由策略 | 调度时选哪个执行器 | 10 种内置策略 | 无(分片归属已由协商确定) |
| 阻塞处理 | 上次没跑完、本次调度又来了怎么办 | 3 种阻塞处理策略 | misfire + monitorExecution |
| 失效转移 | 执行节点挂了,任务怎么办 | 故障转移路由(发生在下次调度) | failover 自动接管分片 |
| 调度精度 | 最小触发间隔 | Quartz cron,最细秒级 | Quartz cron,最细秒级 |
1.3 主流方案定位表
| 方案 | 架构 | 分片 | 可视化管控 | 部署依赖 | 适用场景 |
|---|---|---|---|---|---|
| Spring @Scheduled | 单机进程内 | ❌ | ❌ | 无 | 单体应用的简单定时 |
| Quartz | 单机 / 集群(JDBC 锁) | ❌ | ❌(需第三方) | 关系型数据库 | 只需 cron 能力、不想引入平台 |
| XXL-Job ⭐ | 中心化 | ✅ 分片广播 | ✅ 完善 Admin 控制台 | MySQL | 中小团队、多业务线统一调度,国内最主流 |
| ElasticJob ⭐ | 去中心化 | ✅ 原生分片 | ⚠️ 可选 UI 控制台 | ZooKeeper | 数据分片批处理、弹性伸缩、无单点诉求 |
| PowerJob | 中心化 + 去中心化混合 | ✅ MapReduce / 分片 | ✅ | MySQL / 多存储 | 复杂编排、超大吞吐批处理 |
一句话概括 :
@Scheduled是玩具、Quartz 是轮子、XXL-Job 是平台 (把调度做成产品)、ElasticJob 是框架(把弹性调度做成组件)。本文聚焦后两者。
1.4 中心化 vs 去中心化两大流派
#mermaid-svg-oNrORezyZtiki1Tn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oNrORezyZtiki1Tn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oNrORezyZtiki1Tn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oNrORezyZtiki1Tn .error-icon{fill:#552222;}#mermaid-svg-oNrORezyZtiki1Tn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oNrORezyZtiki1Tn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oNrORezyZtiki1Tn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oNrORezyZtiki1Tn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oNrORezyZtiki1Tn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oNrORezyZtiki1Tn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oNrORezyZtiki1Tn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oNrORezyZtiki1Tn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oNrORezyZtiki1Tn .marker.cross{stroke:#333333;}#mermaid-svg-oNrORezyZtiki1Tn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oNrORezyZtiki1Tn p{margin:0;}#mermaid-svg-oNrORezyZtiki1Tn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oNrORezyZtiki1Tn .cluster-label text{fill:#333;}#mermaid-svg-oNrORezyZtiki1Tn .cluster-label span{color:#333;}#mermaid-svg-oNrORezyZtiki1Tn .cluster-label span p{background-color:transparent;}#mermaid-svg-oNrORezyZtiki1Tn .label text,#mermaid-svg-oNrORezyZtiki1Tn span{fill:#333;color:#333;}#mermaid-svg-oNrORezyZtiki1Tn .node rect,#mermaid-svg-oNrORezyZtiki1Tn .node circle,#mermaid-svg-oNrORezyZtiki1Tn .node ellipse,#mermaid-svg-oNrORezyZtiki1Tn .node polygon,#mermaid-svg-oNrORezyZtiki1Tn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oNrORezyZtiki1Tn .rough-node .label text,#mermaid-svg-oNrORezyZtiki1Tn .node .label text,#mermaid-svg-oNrORezyZtiki1Tn .image-shape .label,#mermaid-svg-oNrORezyZtiki1Tn .icon-shape .label{text-anchor:middle;}#mermaid-svg-oNrORezyZtiki1Tn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oNrORezyZtiki1Tn .rough-node .label,#mermaid-svg-oNrORezyZtiki1Tn .node .label,#mermaid-svg-oNrORezyZtiki1Tn .image-shape .label,#mermaid-svg-oNrORezyZtiki1Tn .icon-shape .label{text-align:center;}#mermaid-svg-oNrORezyZtiki1Tn .node.clickable{cursor:pointer;}#mermaid-svg-oNrORezyZtiki1Tn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oNrORezyZtiki1Tn .arrowheadPath{fill:#333333;}#mermaid-svg-oNrORezyZtiki1Tn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oNrORezyZtiki1Tn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oNrORezyZtiki1Tn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oNrORezyZtiki1Tn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oNrORezyZtiki1Tn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oNrORezyZtiki1Tn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oNrORezyZtiki1Tn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oNrORezyZtiki1Tn .cluster text{fill:#333;}#mermaid-svg-oNrORezyZtiki1Tn .cluster span{color:#333;}#mermaid-svg-oNrORezyZtiki1Tn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oNrORezyZtiki1Tn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oNrORezyZtiki1Tn rect.text{fill:none;stroke-width:0;}#mermaid-svg-oNrORezyZtiki1Tn .icon-shape,#mermaid-svg-oNrORezyZtiki1Tn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oNrORezyZtiki1Tn .icon-shape p,#mermaid-svg-oNrORezyZtiki1Tn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oNrORezyZtiki1Tn .icon-shape .label rect,#mermaid-svg-oNrORezyZtiki1Tn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oNrORezyZtiki1Tn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oNrORezyZtiki1Tn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oNrORezyZtiki1Tn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 去中心化(ElasticJob)
节点 A = 调度器 + 执行器
ZooKeeper
协调分片 / 选主 / 注册
节点 B = 调度器 + 执行器
中心化(XXL-Job)
RPC 下发任务
结果回调
调度中心 Admin
统一触发 + 路由决策
执行器 A / B / C
只负责执行 + 汇报
| 维度 | 中心化(XXL-Job) | 去中心化(ElasticJob) |
|---|---|---|
| 调度决策位置 | 集中在调度中心 | 分散在每个节点,靠 ZK 达成一致 |
| 单点风险 | 调度中心(可多实例 + DB 行锁 HA) | 无中心节点,但强依赖 ZK 集群可用性 |
| 分片方式 | 广播全部执行器,业务按 index/total 自行切片 |
框架协商分片归属,自动重平衡 |
| 运维体验 | 控制台完善,开箱即用 | 需 ZK 运维能力,控制台需另装 |
| 学习成本 | 低(概念少,半小时上手) | 中高(需理解 ZK 节点模型) |
关键认知 :中心化胜在简单可控、排障直观 (所有状态集中在 Admin 一张表里),去中心化胜在无单点、天然弹性(节点上下线分片自动迁移),代价是引入 ZooKeeper 这套额外的基础设施。相关生态全景见 \[springcloude全家桶知识大纲](5.9 节)。
2. XXL-Job 设计思想与核心机制 ★★★
2.1 中心化架构与通信模型
组件组成:
| 组件 | 形态 | 职责 |
|---|---|---|
xxl-job-admin |
独立部署的 Spring Boot Web 应用 | 任务 CRUD、触发调度、日志存储、告警、控制台 |
xxl-job-core |
业务服务引入的 Maven 依赖 | 内嵌 Jetty Server 暴露执行服务、管理 JobThread |
| MySQL | 存储 | 任务表、执行器注册表、调度日志、锁表 |
通信模型 :执行器启动时内嵌 Jetty Server,调度中心通过自研 RPC(HTTP + Hessian 序列化)发起调用------执行器不需要注册中心、不需要长连接,天然穿越防火墙/NAT,这是它与 Dubbo/gRPC 类方案最大的差异点。
#mermaid-svg-exbf2CCJYW9vgRZY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-exbf2CCJYW9vgRZY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-exbf2CCJYW9vgRZY .error-icon{fill:#552222;}#mermaid-svg-exbf2CCJYW9vgRZY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-exbf2CCJYW9vgRZY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-exbf2CCJYW9vgRZY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-exbf2CCJYW9vgRZY .marker.cross{stroke:#333333;}#mermaid-svg-exbf2CCJYW9vgRZY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-exbf2CCJYW9vgRZY p{margin:0;}#mermaid-svg-exbf2CCJYW9vgRZY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-exbf2CCJYW9vgRZY .cluster-label text{fill:#333;}#mermaid-svg-exbf2CCJYW9vgRZY .cluster-label span{color:#333;}#mermaid-svg-exbf2CCJYW9vgRZY .cluster-label span p{background-color:transparent;}#mermaid-svg-exbf2CCJYW9vgRZY .label text,#mermaid-svg-exbf2CCJYW9vgRZY span{fill:#333;color:#333;}#mermaid-svg-exbf2CCJYW9vgRZY .node rect,#mermaid-svg-exbf2CCJYW9vgRZY .node circle,#mermaid-svg-exbf2CCJYW9vgRZY .node ellipse,#mermaid-svg-exbf2CCJYW9vgRZY .node polygon,#mermaid-svg-exbf2CCJYW9vgRZY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-exbf2CCJYW9vgRZY .rough-node .label text,#mermaid-svg-exbf2CCJYW9vgRZY .node .label text,#mermaid-svg-exbf2CCJYW9vgRZY .image-shape .label,#mermaid-svg-exbf2CCJYW9vgRZY .icon-shape .label{text-anchor:middle;}#mermaid-svg-exbf2CCJYW9vgRZY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-exbf2CCJYW9vgRZY .rough-node .label,#mermaid-svg-exbf2CCJYW9vgRZY .node .label,#mermaid-svg-exbf2CCJYW9vgRZY .image-shape .label,#mermaid-svg-exbf2CCJYW9vgRZY .icon-shape .label{text-align:center;}#mermaid-svg-exbf2CCJYW9vgRZY .node.clickable{cursor:pointer;}#mermaid-svg-exbf2CCJYW9vgRZY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-exbf2CCJYW9vgRZY .arrowheadPath{fill:#333333;}#mermaid-svg-exbf2CCJYW9vgRZY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-exbf2CCJYW9vgRZY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-exbf2CCJYW9vgRZY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-exbf2CCJYW9vgRZY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-exbf2CCJYW9vgRZY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-exbf2CCJYW9vgRZY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-exbf2CCJYW9vgRZY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-exbf2CCJYW9vgRZY .cluster text{fill:#333;}#mermaid-svg-exbf2CCJYW9vgRZY .cluster span{color:#333;}#mermaid-svg-exbf2CCJYW9vgRZY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-exbf2CCJYW9vgRZY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-exbf2CCJYW9vgRZY rect.text{fill:none;stroke-width:0;}#mermaid-svg-exbf2CCJYW9vgRZY .icon-shape,#mermaid-svg-exbf2CCJYW9vgRZY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-exbf2CCJYW9vgRZY .icon-shape p,#mermaid-svg-exbf2CCJYW9vgRZY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-exbf2CCJYW9vgRZY .icon-shape .label rect,#mermaid-svg-exbf2CCJYW9vgRZY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-exbf2CCJYW9vgRZY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-exbf2CCJYW9vgRZY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-exbf2CCJYW9vgRZY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行器集群(内嵌业务服务)
MySQL
xxl-job-admin 调度中心集群(多实例部署)
RPC 调用(HTTP + Hessian)
select for update
Quartz 调度器
cron 到点触发
JobTriggerPoolHelper
快线程池 200 / 慢线程池 100
ExecutorRouter
10 种路由策略
运维 API
任务 CRUD / 手动触发 / 日志查询
xxl_job_info 任务表
xxl_job_registry 执行器注册表
xxl_job_log 调度日志
xxl_job_lock 行锁
防集群重复调度
内置 Jetty Server
ExecutorBizImpl
JobThread 任务线程
每个任务一条专属线程
IJobHandler
BEAN / GLUE / Script
执行器注册与心跳 :执行器启动后调用 Admin 接口写入 xxl_job_registry 表;每 30 秒 心跳续期,90 秒未心跳被自动摘除。路由策略只在"在线"执行器中选地址,因此节点扩缩容无需改任何配置。
调度一致性 :调度中心支持多实例部署做 HA。同一任务的触发前会 select ... for update 行锁预读任务行,保证集群下同一调度时刻只有一个 Admin 实例真正触发,其余实例放弃。
接入示例(Spring Boot 执行器完整五步):
xml
<!-- ① 引入依赖:执行器只需这一个核心包 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.2</version> <!-- JDK8 项目用 2.4.x;3.x 版本线要求 JDK17 -->
</dependency>
yaml
# ② 执行器配置
xxl:
job:
accessToken: default_token # 与调度中心 xxl.job.accessToken 保持一致
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心地址
executor:
appname: order-service # 执行器名称,与调度中心「执行器管理」里的一致
port: 9999 # 内嵌 Jetty 端口
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
java
// ③ 注册执行器 Bean(Spring Boot 配置类)
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken:}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appname);
executor.setPort(port);
executor.setAccessToken(accessToken);
executor.setLogPath(logPath);
executor.setLogRetentionDays(logRetentionDays);
return executor;
}
}
java
// ④ 第一个任务:调度中心新建任务时「JobHandler」填 demoJobHandler
@Component
public class DemoJobHandler {
@XxlJob("demoJobHandler")
public void demoJob() {
XxlJobHelper.log("开始执行 demo 任务,任务参数:%s", XxlJobHelper.getJobParam());
}
}
yaml
# ⑤ 调度中心侧(xxl-job-admin)核心配置:先执行 doc/db/tables_xxl_job.sql 建库
server:
port: 8080
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
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
xxl:
job:
accessToken: default_token
控制台三步走:执行器管理 新增 order-service(自动注册后在线)→ 任务管理 新增任务(JobHandler 填 demoJobHandler)→ 执行一次,在「调度日志」查看执行结果。
2.2 调度线程模型与快慢线程池
调度中心不直接让 Quartz 线程执行触发动作,而是转发给自有的触发线程池------这就是快慢线程池设计:
| 线程池 | 默认线程数 | 处理对象 | 目的 |
|---|---|---|---|
快线程池 fastTriggerPool |
200 | 1 分钟内调度耗时 ≤ 500ms 的任务 | 保证常规任务毫秒级触发,不被拖累 |
慢线程池 slowTriggerPool |
100 | 1 分钟内调度耗时 > 500ms 且达 10 次的任务 | 隔离慢任务,防止"慢任务占满线程、全体任务触发延迟" |
关键认知 :快慢分离解决的是木桶效应------调度触发本身是 IO 密集(查库、选执行器、发 RPC),一个执行器网络抖动就可能导致触发线程阻塞。把"坏孩子"关进慢线程池,是调度可用性的第一道防线。执行器侧同样做了快慢线程池隔离(新版本起),防止慢任务阻塞其他任务的执行线程。
2.3 任务执行线程模型与阻塞处理策略
执行器侧:每个任务一条专属 JobThread(懒创建,串行消费触发队列),任务之间互不阻塞;同一个任务的多次触发在该线程内排队串行执行。
当"上次没跑完、本次调度又来了",由阻塞处理策略决定行为:
| 策略 | 行为 | 适用场景 |
|---|---|---|
单机串行 SERIAL_EXECUTION(默认) |
本次触发排队等待上次执行完 | 任务必须顺序执行,如对账 |
丢弃后续调度 DISCARD_LATER |
上次未跑完,本次直接丢弃并记告警 | 允许偶尔漏跑的统计类任务 |
覆盖之前调度 COVER_EARLY |
清空队列中未执行的触发,只跑最新一次 | 只关心最新状态的任务,如缓存刷新 |
配套的还有任务超时 配置:执行超时后执行器向 JobThread 发送中断信号(interrupt),业务线程若响应中断则退出,日志标记超时------注意它不强制 kill 线程 ,业务代码需要响应 InterruptedException。
2.4 十种路由策略全景
调度中心按路由策略从"在线执行器列表"中选出一个(或多个)地址:
| 路由策略 | 说明 | 适用场景 |
|---|---|---|
第一个 FIRST |
固定选列表中第一个 | 测试 / 单执行器环境 |
最后一个 LAST |
固定选列表中最后一个 | 固定机器执行 |
轮询 ROUND |
依次循环选择 | 负载均摊,最常用 |
随机 RANDOM |
随机选择 | 简单均摊 |
一致性 HASH CONSISTENT_HASH |
对任务参数哈希后选节点 | 同一参数总路由到同一执行器(如按 userId 做本地缓存) |
最不经常使用 LEAST_FREQUENTLY_USED |
按使用频率选择 | 按频率均摊 |
最近最久未使用 LEAST_RECENTLY_USED |
按最近使用时间选择 | 类似 LRU 均摊 |
故障转移 FAILOVER |
心跳检测,失败后换节点重试,直到成功 | 对可用性要求高的任务 |
忙碌转移 BUSYOVER |
按执行器阻塞队列长度选最空闲节点 | 任务执行耗时差异大时 |
分片广播 SHARDING_BROADCAST |
广播给所有在线执行器 | 分片并行处理,见 2.5 |
自定义路由策略示例 :有特殊选节点诉求(机房亲和、按机器权重等)时,在调度中心工程 实现 ExecutorRouter 并注册到 ExecutorRouteStrategyEnum,重启后任务配置的「路由策略」下拉即可选择:
java
/**
* 自定义路由策略:示例简化------返回第一个在线执行器。
* 真实场景可在此按机房标签、机器权重过滤后返回目标地址。
* 位置:调度中心 xxl-job-admin 工程
*/
@Component
public class IdcAwareExecutorRouter extends ExecutorRouter {
@Override
public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
if (CollectionUtils.isEmpty(addressList)) {
return new ReturnT<>(ReturnT.FAIL_CODE, "无在线执行器");
}
// TODO 按机房/权重过滤,示例直接返回第一个
String address = addressList.get(0);
return new ReturnT<>(address);
}
}
2.5 分片广播机制
分片广播是 XXL-Job 处理大数据量任务的核心手段:调度中心把任务广播给所有在线执行器 ,每个执行器拿到自己的分片号与分片总数,按 index/total 对数据做取模切片:
java
@Component
public class OrderTimeoutJobHandler {
@XxlJob("orderTimeoutCheckJob") // 调度中心配置:路由策略=分片广播
public void handle() {
int shardIndex = XxlJobHelper.getShardIndex(); // 当前分片号 0 ~ total-1
int shardTotal = XxlJobHelper.getShardTotal(); // 分片总数 = 在线执行器数
List<Order> orders = orderService.listTimeoutOrders(shardIndex, shardTotal);
for (Order order : orders) {
orderService.close(order); // 业务侧通常按 order.getId() % shardTotal 过滤
}
XxlJobHelper.log("分片[%d/%d] 处理超时订单 %d 个", shardIndex, shardTotal, orders.size());
}
}
| 要点 | 说明 |
|---|---|
| 分片参数获取 | XxlJobHelper.getShardIndex() / getShardTotal() |
| 分片总数 | 等于当前在线执行器数量,是动态的 |
| 数据切片方式 | 业务自行约定,最常见是 id % total == index |
| ⚠️ 注意事项 | 执行器扩缩容后分片总数变化,数据切片会重排,业务必须幂等 |
2.6 失败重试、调度过期与告警
| 机制 | 配置 | 行为 |
|---|---|---|
| 失败重试次数 | 任务配置 0~N | 执行失败后自动重试 N 次,仍失败才进入告警 |
| 调度过期策略 | 忽略 / 立即补偿执行一次 | 调度中心宕机恢复后,对宕机期间错过的调度:直接忽略,或立即补跑一次 |
| 报警 | 邮件报警(可扩展) | 调度线程池默认约 30s 轮询一次失败日志,触发邮件;实现 JobAlarm 接口可换成钉钉/企微 |
| 任务超时 | 1min / 10min 等 | 见 2.3,超时中断业务线程 |
| 阻塞处理策略 | 3 种 | 见 2.3 |
自定义告警示例 :默认实现是邮件告警(EmailJobAlarm),接入钉钉/企微只需在调度中心工程 实现 JobAlarm 并注册为 Bean:
java
/**
* 自定义告警:任务失败发钉钉群机器人
* 位置:调度中心 xxl-job-admin 工程(注册为 Bean 后替换默认邮件告警)
*/
@Component
public class DingTalkJobAlarm implements JobAlarm {
@Override
public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog) {
if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) {
String content = String.format(
"【任务告警】%s 执行失败,失败原因:%s,调度日志ID:%d",
info.getJobDesc(), jobLog.getHandleMsg(), jobLog.getId());
dingTalkClient.send(content); // 调用钉钉群机器人 Webhook
}
return true;
}
}
2.7 GLUE 动态任务
XXL-Job 任务有 BEAN 与 GLUE 两种模式:
| 模式 | 任务逻辑存放位置 | 修改成本 | 适用场景 |
|---|---|---|---|
| BEAN 模式 ⭐ | 业务代码里,@XxlJob("name") 注解注册 |
改代码 → 发版 | 常规业务任务 |
| GLUE 模式 | 调度中心控制台在线编辑,存于 xxl_job_logglue 表 |
在线改 → 立即生效,支持历史版本回滚 | 临时任务、运营脚本、免发版任务 |
GLUE 支持源码与脚本两类:GLUE(Java) 用 Groovy 动态编译热加载;Shell / Python / PHP / Node.js / PowerShell 由执行器拉起对应解释器执行。
GLUE(Java) 在控制台的源码长这样(执行器侧无需发版,保存后立即生效,历史版本可回滚):
java
// 控制台 GLUE(Java) 模式源码(Groovy 动态编译,运行在执行器 JVM 内)
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public void execute() throws Exception {
XxlJobHelper.log("XXL-JOB, GLUE(Java) 任务执行开始。");
// 业务逻辑:需要访问 Spring Bean 时,可通过执行器侧暴露的
// SpringUtil 等静态工具类获取 ApplicationContext
}
}
GLUE(Shell) 脚本示例(执行器落地为脚本文件后由对应解释器执行):
bash
#!/bin/bash
# GLUE(Shell):清理 7 天前的应用日志
echo "开始清理过期日志:$(date '+%F %T')"
find /data/applogs -name "*.log" -mtime +7 -delete
echo "清理完成"
关键认知 :GLUE 是 XXL-Job 从"调度平台"变成"运维生产力工具"的关键设计------运营提一个"改 XX 数据"的临时需求,不用走研发发版流程。但也带来代码散落、不可测试的治理成本,规范做法是 GLUE 只放一次性脚本,长期任务沉淀为 BEAN 模式。
3. ElasticJob 设计思想与核心机制 ★★★
3.1 去中心化与 ZooKeeper 协调
演进脉络 :当当开源(2.x 时代)→ 2020 年捐赠 Apache → Apache ShardingSphere ElasticJob 3.x(当前 3.0.5)。2.x 的 ElasticJob-Cloud(基于 Mesos 的容器化资源治理)在 3.x 中已移除,Apache 版本聚焦 Lite 形态。
ElasticJob 的架构哲学与 XXL-Job 完全相反:没有独立的调度中心 。每个引入 elasticjob-lite 依赖的应用节点自带 Quartz 调度器 + 执行器,各节点通过 ZooKeeper 达成"谁执行哪一片"的共识:
#mermaid-svg-38EjLt4KJbPC92N8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-38EjLt4KJbPC92N8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-38EjLt4KJbPC92N8 .error-icon{fill:#552222;}#mermaid-svg-38EjLt4KJbPC92N8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-38EjLt4KJbPC92N8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-38EjLt4KJbPC92N8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-38EjLt4KJbPC92N8 .marker.cross{stroke:#333333;}#mermaid-svg-38EjLt4KJbPC92N8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-38EjLt4KJbPC92N8 p{margin:0;}#mermaid-svg-38EjLt4KJbPC92N8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-38EjLt4KJbPC92N8 .cluster-label text{fill:#333;}#mermaid-svg-38EjLt4KJbPC92N8 .cluster-label span{color:#333;}#mermaid-svg-38EjLt4KJbPC92N8 .cluster-label span p{background-color:transparent;}#mermaid-svg-38EjLt4KJbPC92N8 .label text,#mermaid-svg-38EjLt4KJbPC92N8 span{fill:#333;color:#333;}#mermaid-svg-38EjLt4KJbPC92N8 .node rect,#mermaid-svg-38EjLt4KJbPC92N8 .node circle,#mermaid-svg-38EjLt4KJbPC92N8 .node ellipse,#mermaid-svg-38EjLt4KJbPC92N8 .node polygon,#mermaid-svg-38EjLt4KJbPC92N8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-38EjLt4KJbPC92N8 .rough-node .label text,#mermaid-svg-38EjLt4KJbPC92N8 .node .label text,#mermaid-svg-38EjLt4KJbPC92N8 .image-shape .label,#mermaid-svg-38EjLt4KJbPC92N8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-38EjLt4KJbPC92N8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-38EjLt4KJbPC92N8 .rough-node .label,#mermaid-svg-38EjLt4KJbPC92N8 .node .label,#mermaid-svg-38EjLt4KJbPC92N8 .image-shape .label,#mermaid-svg-38EjLt4KJbPC92N8 .icon-shape .label{text-align:center;}#mermaid-svg-38EjLt4KJbPC92N8 .node.clickable{cursor:pointer;}#mermaid-svg-38EjLt4KJbPC92N8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-38EjLt4KJbPC92N8 .arrowheadPath{fill:#333333;}#mermaid-svg-38EjLt4KJbPC92N8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-38EjLt4KJbPC92N8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-38EjLt4KJbPC92N8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-38EjLt4KJbPC92N8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-38EjLt4KJbPC92N8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-38EjLt4KJbPC92N8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-38EjLt4KJbPC92N8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-38EjLt4KJbPC92N8 .cluster text{fill:#333;}#mermaid-svg-38EjLt4KJbPC92N8 .cluster span{color:#333;}#mermaid-svg-38EjLt4KJbPC92N8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-38EjLt4KJbPC92N8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-38EjLt4KJbPC92N8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-38EjLt4KJbPC92N8 .icon-shape,#mermaid-svg-38EjLt4KJbPC92N8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-38EjLt4KJbPC92N8 .icon-shape p,#mermaid-svg-38EjLt4KJbPC92N8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-38EjLt4KJbPC92N8 .icon-shape .label rect,#mermaid-svg-38EjLt4KJbPC92N8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-38EjLt4KJbPC92N8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-38EjLt4KJbPC92N8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-38EjLt4KJbPC92N8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实例 B
本地 Quartz
LiteJobFacade
业务 ElasticJob 实现
实例 A(调度器 + 执行器)
本地 Quartz
cron 到点触发
LiteJobFacade
执行本实例分片
业务 ElasticJob 实现
SimpleJob / DataflowJob
ZooKeeper 注册中心集群
/namespace/jobName/config
作业配置(cron、分片数)
/instances
实例上线注册(临时节点)
/sharding/0/instance
分片项归属
/leader/election/instance
主节点选举
ZooKeeper 节点结构:
| ZK 节点路径 | 用途 |
|---|---|
/{namespace}/{jobName}/config |
作业配置(cron、分片数、分片参数等),配置变更实时下发 |
/{namespace}/{jobName}/instances/{instanceId} |
实例注册(临时节点,实例下线自动删除) |
/{namespace}/{jobName}/sharding/{item}/instance |
分片项 item 归属哪个实例 |
/{namespace}/{jobName}/leader/election/instance |
主节点选举(用于协调失效转移、重新分片) |
/{namespace}/{jobName}/servers/{instanceId} |
实例状态(ENABLED / DISABLED) |
主节点:节点间选举产生一个 leader,负责失效转移与分片变更的协调;leader 崩溃后 ZK 临时节点消失,其余节点自动重新选举------这是"无中心"得以成立的关键。
关键认知 :去中心化 ≠ 不依赖任何中心服务------ZK 就是它的"隐性中心"。ZK 集群不可用时,已运行实例照常执行,但实例上下线、重新分片、失效转移全部停摆。选 ElasticJob 的前提是具备 ZK 运维能力。
3.2 分片与弹性伸缩
ElasticJob 的分片是框架原生能力:作业声明分片总数 N,框架把 N 个分片项(shardingItem)分配给在线的 M 个实例,业务只关心自己拿到的分片项对应的数据:
yaml
# Spring Boot Starter 方式配置(3.x)
elasticjob:
reg-center:
server-lists: localhost:2181
namespace: elasticjob-demo
jobs:
orderTimeoutJob:
elastic-job-class: com.example.OrderTimeoutJob # 实现 SimpleJob 的 Bean
cron: 0 0 2 * * ? # 每天凌晨 2 点
sharding-total-count: 3 # 分 3 片
sharding-item-parameters: 0=Beijing,1=Shanghai,2=Guangzhou # 分片项 → 业务参数
job-error-handler-type: LOG
java
@Component
public class OrderTimeoutJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
String city = context.getShardingParameter(); // 如 "Beijing"
int item = context.getShardingItem(); // 0 / 1 / 2
orderService.closeTimeoutOrders(city);
}
}
分片策略:
| 策略类 | 分配规则 |
|---|---|
AverageAllocationJobShardingStrategy |
平均分配(余数分给前面的实例) |
OdevitySortByNameJobShardingStrategy |
按实例名哈希的奇偶分 |
RotateServerByNameJobShardingStrategy |
按实例名轮转 |
自定义实现 JobShardingStrategy |
按机房、权重等业务维度分 |
弹性伸缩三件套:
| 能力 | 触发 | 行为 |
|---|---|---|
| 自动重新分片 | 实例上线/下线 → instances 节点变化 |
监听器触发重新分片,分片无缝迁移,无需人工干预 |
失效转移 failover |
实例崩溃(临时节点消失) | 主节点协调,把崩溃实例的分片转移给存活实例 |
错过任务重执行 misfire |
错过 cron 且 monitorExecution=true |
补跑错过的调度;若上次仍在执行则跳过,避免重复 |
3.3 作业类型与事件追踪
| 作业类型 | 接口 / 方式 | 说明 |
|---|---|---|
| SimpleJob ⭐ | execute(ShardingContext) |
最常用,一次执行一批数据 |
| DataflowJob | fetchData / processData |
流式处理:fetchData 返回 null 即结束本轮,适合逐条拉取处理 |
| ScriptJob | 命令行脚本 | 沿用 2.x 能力,执行 shell 脚本 |
| HttpJob | HTTP 请求配置 | 3.x 新增,配置 URL/方法/参数即可调用远程接口 |
事件追踪 :作业每次执行的开始/成功/失败都会发布事件(JobEvent)到事件总线,可落地到 RDB (配置事件追踪数据源)或日志,用于审计与排障。
作业监听器 :ElasticJobListener(作业执行前/后回调)、DistributeOnceElasticJobListener(所有分片都执行完才触发一次,适合做全局汇总)。
4. 主线流程剖析 ★★★
4.1 XXL-Job 一次调度的完整链路
#mermaid-svg-2nR2TLKJzvIORowv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2nR2TLKJzvIORowv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2nR2TLKJzvIORowv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2nR2TLKJzvIORowv .error-icon{fill:#552222;}#mermaid-svg-2nR2TLKJzvIORowv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2nR2TLKJzvIORowv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2nR2TLKJzvIORowv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2nR2TLKJzvIORowv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2nR2TLKJzvIORowv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2nR2TLKJzvIORowv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2nR2TLKJzvIORowv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2nR2TLKJzvIORowv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2nR2TLKJzvIORowv .marker.cross{stroke:#333333;}#mermaid-svg-2nR2TLKJzvIORowv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2nR2TLKJzvIORowv p{margin:0;}#mermaid-svg-2nR2TLKJzvIORowv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2nR2TLKJzvIORowv .cluster-label text{fill:#333;}#mermaid-svg-2nR2TLKJzvIORowv .cluster-label span{color:#333;}#mermaid-svg-2nR2TLKJzvIORowv .cluster-label span p{background-color:transparent;}#mermaid-svg-2nR2TLKJzvIORowv .label text,#mermaid-svg-2nR2TLKJzvIORowv span{fill:#333;color:#333;}#mermaid-svg-2nR2TLKJzvIORowv .node rect,#mermaid-svg-2nR2TLKJzvIORowv .node circle,#mermaid-svg-2nR2TLKJzvIORowv .node ellipse,#mermaid-svg-2nR2TLKJzvIORowv .node polygon,#mermaid-svg-2nR2TLKJzvIORowv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2nR2TLKJzvIORowv .rough-node .label text,#mermaid-svg-2nR2TLKJzvIORowv .node .label text,#mermaid-svg-2nR2TLKJzvIORowv .image-shape .label,#mermaid-svg-2nR2TLKJzvIORowv .icon-shape .label{text-anchor:middle;}#mermaid-svg-2nR2TLKJzvIORowv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2nR2TLKJzvIORowv .rough-node .label,#mermaid-svg-2nR2TLKJzvIORowv .node .label,#mermaid-svg-2nR2TLKJzvIORowv .image-shape .label,#mermaid-svg-2nR2TLKJzvIORowv .icon-shape .label{text-align:center;}#mermaid-svg-2nR2TLKJzvIORowv .node.clickable{cursor:pointer;}#mermaid-svg-2nR2TLKJzvIORowv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2nR2TLKJzvIORowv .arrowheadPath{fill:#333333;}#mermaid-svg-2nR2TLKJzvIORowv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2nR2TLKJzvIORowv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2nR2TLKJzvIORowv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2nR2TLKJzvIORowv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2nR2TLKJzvIORowv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2nR2TLKJzvIORowv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2nR2TLKJzvIORowv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2nR2TLKJzvIORowv .cluster text{fill:#333;}#mermaid-svg-2nR2TLKJzvIORowv .cluster span{color:#333;}#mermaid-svg-2nR2TLKJzvIORowv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2nR2TLKJzvIORowv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2nR2TLKJzvIORowv rect.text{fill:none;stroke-width:0;}#mermaid-svg-2nR2TLKJzvIORowv .icon-shape,#mermaid-svg-2nR2TLKJzvIORowv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2nR2TLKJzvIORowv .icon-shape p,#mermaid-svg-2nR2TLKJzvIORowv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2nR2TLKJzvIORowv .icon-shape .label rect,#mermaid-svg-2nR2TLKJzvIORowv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2nR2TLKJzvIORowv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2nR2TLKJzvIORowv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2nR2TLKJzvIORowv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① Quartz 到点
触发 scheduleJobId
② 快/慢线程池
取出触发线程
③ 行锁预读
select for update
④ 路由策略
选定执行器地址
⑤ RPC 下发
ExecutorBiz.run
⑥ JobThread
串行执行任务
⑦ 回调调度中心
写日志 + 更新状态
⑧ 失败处理
按次数重试 + 告警
- Quartz 到点 :Admin 内嵌 Quartz,cron 到点后拿到 scheduleJobId,交给
JobTriggerPoolHelper,按任务"快/慢"属性选择线程池。 - 行锁预读 :
XxlJobTrigger.processTrigger用select ... for update预读任务行------集群多 Admin 场景下只有抢到行锁的实例继续,其余放弃,保证不重复触发。 - 选择执行器 :查
xxl_job_registry拿到在线执行器地址列表,按任务配置的路由策略(默认轮询)选定目标地址;分片广播则取全部地址。 - RPC 下发 :通过
ExecutorBiz动态代理(HTTP + Hessian)调用执行器内嵌 Jetty 暴露的run()接口。 - 执行器执行:执行器将触发入队到该任务专属 JobThread,串行执行;GLUE 模式则先取最新源码(Groovy 编译)再执行;注入分片参数、超时监控。
- 结果回调 :执行结果(成功/失败 + 日志)回调 Admin,写
xxl_job_log,更新任务状态。 - 失败处理 :按配置的重试次数自动重试;仍失败触发
JobAlarm告警,调度中心约 30s 轮询一次失败日志。
4.2 ElasticJob 一次执行的完整链路
#mermaid-svg-Kzgvm3n7RILC3mp7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kzgvm3n7RILC3mp7 .error-icon{fill:#552222;}#mermaid-svg-Kzgvm3n7RILC3mp7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kzgvm3n7RILC3mp7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .marker.cross{stroke:#333333;}#mermaid-svg-Kzgvm3n7RILC3mp7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kzgvm3n7RILC3mp7 p{margin:0;}#mermaid-svg-Kzgvm3n7RILC3mp7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster-label text{fill:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster-label span{color:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster-label span p{background-color:transparent;}#mermaid-svg-Kzgvm3n7RILC3mp7 .label text,#mermaid-svg-Kzgvm3n7RILC3mp7 span{fill:#333;color:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .node rect,#mermaid-svg-Kzgvm3n7RILC3mp7 .node circle,#mermaid-svg-Kzgvm3n7RILC3mp7 .node ellipse,#mermaid-svg-Kzgvm3n7RILC3mp7 .node polygon,#mermaid-svg-Kzgvm3n7RILC3mp7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .rough-node .label text,#mermaid-svg-Kzgvm3n7RILC3mp7 .node .label text,#mermaid-svg-Kzgvm3n7RILC3mp7 .image-shape .label,#mermaid-svg-Kzgvm3n7RILC3mp7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Kzgvm3n7RILC3mp7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .rough-node .label,#mermaid-svg-Kzgvm3n7RILC3mp7 .node .label,#mermaid-svg-Kzgvm3n7RILC3mp7 .image-shape .label,#mermaid-svg-Kzgvm3n7RILC3mp7 .icon-shape .label{text-align:center;}#mermaid-svg-Kzgvm3n7RILC3mp7 .node.clickable{cursor:pointer;}#mermaid-svg-Kzgvm3n7RILC3mp7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .arrowheadPath{fill:#333333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kzgvm3n7RILC3mp7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Kzgvm3n7RILC3mp7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kzgvm3n7RILC3mp7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster text{fill:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 .cluster span{color:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Kzgvm3n7RILC3mp7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Kzgvm3n7RILC3mp7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Kzgvm3n7RILC3mp7 .icon-shape,#mermaid-svg-Kzgvm3n7RILC3mp7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kzgvm3n7RILC3mp7 .icon-shape p,#mermaid-svg-Kzgvm3n7RILC3mp7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Kzgvm3n7RILC3mp7 .icon-shape .label rect,#mermaid-svg-Kzgvm3n7RILC3mp7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kzgvm3n7RILC3mp7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Kzgvm3n7RILC3mp7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Kzgvm3n7RILC3mp7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 实例启动
注册 ZK 临时节点
② 主节点选举
leader/election
③ 分片计算
写入 sharding 节点
④ 监听分片变更
取回本实例分片
⑤ 本地 Quartz
cron 到点
⑥ LiteJobFacade
执行分片业务
⑦ 上报状态
事件追踪
⑧ 实例异常
失效转移 + 重新分片
- 实例注册 :作业实例启动,向 ZK 注册
instances临时节点,并拉取config节点下的作业配置。 - 主节点选举 :各实例竞争
leader/election,产生一个主节点,负责协调分片与失效转移。 - 分片计算 :主节点(或触发分片变更的实例)按配置的分片策略计算 N 个分片项的归属,写入
sharding节点。 - 监听分片 :各实例监听
sharding节点变化,取回属于自己的分片项(含sharding-item-parameters中的业务参数)。 - 本地触发 :cron 到点,每个实例的本地 Quartz 各自触发------没有远程调度调用,触发天然无延迟。
- 执行分片业务 :
LiteJobFacade组装ShardingContext,调用业务实现的execute()。 - 状态上报:执行结果写入 ZK 执行状态节点,事件发布到事件追踪(RDB/日志)。
- 异常自愈:实例崩溃 → 临时节点消失 → 主节点协调失效转移,把其分片迁给存活实例;实例上线同理反向重平衡。
4.3 两大框架链路对比
| 维度 | XXL-Job | ElasticJob |
|---|---|---|
| 触发方 | 调度中心统一触发 | 各实例本地 Quartz 各自触发 |
| 触发链路 | 中心 → RPC → 执行器(有网络开销) | 本地直接触发(无网络开销) |
| 调度决策 | 中心化(路由策略选地址) | 去中心化(分片协商定归属) |
| 一致性保障 | MySQL 行锁 select for update |
ZK 临时节点 + 主节点协调 |
| 单点风险 | 调度中心(多实例 HA 可解) | 无中心,但依赖 ZK 可用性 |
| 弹性分片 | 广播 + 业务自行切片,扩缩容需重算 | 原生分片 + 自动重平衡 |
| 失效转移 | 路由层面(下次调度换节点) | 分片级自动接管 |
5. 核心类关系图
5.1 XXL-Job 核心类关系
#mermaid-svg-QjpftM0mHAkspDJW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QjpftM0mHAkspDJW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QjpftM0mHAkspDJW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QjpftM0mHAkspDJW .error-icon{fill:#552222;}#mermaid-svg-QjpftM0mHAkspDJW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QjpftM0mHAkspDJW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QjpftM0mHAkspDJW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QjpftM0mHAkspDJW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QjpftM0mHAkspDJW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QjpftM0mHAkspDJW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QjpftM0mHAkspDJW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QjpftM0mHAkspDJW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QjpftM0mHAkspDJW .marker.cross{stroke:#333333;}#mermaid-svg-QjpftM0mHAkspDJW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QjpftM0mHAkspDJW p{margin:0;}#mermaid-svg-QjpftM0mHAkspDJW g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-QjpftM0mHAkspDJW g.classGroup text .title{font-weight:bolder;}#mermaid-svg-QjpftM0mHAkspDJW .cluster-label text{fill:#333;}#mermaid-svg-QjpftM0mHAkspDJW .cluster-label span{color:#333;}#mermaid-svg-QjpftM0mHAkspDJW .cluster-label span p{background-color:transparent;}#mermaid-svg-QjpftM0mHAkspDJW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QjpftM0mHAkspDJW .cluster text{fill:#333;}#mermaid-svg-QjpftM0mHAkspDJW .cluster span{color:#333;}#mermaid-svg-QjpftM0mHAkspDJW .nodeLabel,#mermaid-svg-QjpftM0mHAkspDJW .edgeLabel{color:#131300;}#mermaid-svg-QjpftM0mHAkspDJW .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-QjpftM0mHAkspDJW .label text{fill:#131300;}#mermaid-svg-QjpftM0mHAkspDJW .labelBkg{background:#ECECFF;}#mermaid-svg-QjpftM0mHAkspDJW .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-QjpftM0mHAkspDJW .classTitle{font-weight:bolder;}#mermaid-svg-QjpftM0mHAkspDJW .node rect,#mermaid-svg-QjpftM0mHAkspDJW .node circle,#mermaid-svg-QjpftM0mHAkspDJW .node ellipse,#mermaid-svg-QjpftM0mHAkspDJW .node polygon,#mermaid-svg-QjpftM0mHAkspDJW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QjpftM0mHAkspDJW .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW g.clickable{cursor:pointer;}#mermaid-svg-QjpftM0mHAkspDJW g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-QjpftM0mHAkspDJW g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-QjpftM0mHAkspDJW .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-QjpftM0mHAkspDJW .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-QjpftM0mHAkspDJW .dashed-line{stroke-dasharray:3;}#mermaid-svg-QjpftM0mHAkspDJW .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-QjpftM0mHAkspDJW #compositionStart,#mermaid-svg-QjpftM0mHAkspDJW .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #compositionEnd,#mermaid-svg-QjpftM0mHAkspDJW .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #dependencyStart,#mermaid-svg-QjpftM0mHAkspDJW .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #dependencyStart,#mermaid-svg-QjpftM0mHAkspDJW .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #extensionStart,#mermaid-svg-QjpftM0mHAkspDJW .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #extensionEnd,#mermaid-svg-QjpftM0mHAkspDJW .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #aggregationStart,#mermaid-svg-QjpftM0mHAkspDJW .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #aggregationEnd,#mermaid-svg-QjpftM0mHAkspDJW .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #lollipopStart,#mermaid-svg-QjpftM0mHAkspDJW .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW #lollipopEnd,#mermaid-svg-QjpftM0mHAkspDJW .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-QjpftM0mHAkspDJW .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-QjpftM0mHAkspDJW .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QjpftM0mHAkspDJW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QjpftM0mHAkspDJW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QjpftM0mHAkspDJW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 远程调用
XxlJobScheduler\n调度中心启动入口
+init() : 初始化调度器
+destroy()
JobTriggerPoolHelper\n快慢线程池
+fastTriggerPool 快线程池 200
+slowTriggerPool 慢线程池 100
+trigger() : 触发任务
XxlJobTrigger\n调度核心入口
+trigger() : 调度入口
+processTrigger() : 行锁预读 + 触发
ExecutorRouter\n路由策略抽象
+route() : 选择执行器
ExecutorBizClient\nRPC 客户端代理
+run() : 远程执行
ExecutorBizImpl\nJetty 服务端实现
+run() : 接收执行请求
XxlJobExecutor\n执行器启动入口
+start() : 启动内嵌 Jetty
+registJobThread() : 注册任务线程
JobThread\n每任务一条执行线程
+triggerQueue 触发队列
+pushTriggerQueue() : 入队
+run() : 串行消费执行
<<interface>>
IJobHandler\n任务处理器抽象
+execute() : 执行任务
MethodJobHandler\n@XxlJob 注解模式
GlueJobHandler\nGLUE 源码模式
5.2 ElasticJob 核心类关系
#mermaid-svg-tTA20satd2Ombos8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tTA20satd2Ombos8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tTA20satd2Ombos8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tTA20satd2Ombos8 .error-icon{fill:#552222;}#mermaid-svg-tTA20satd2Ombos8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tTA20satd2Ombos8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tTA20satd2Ombos8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tTA20satd2Ombos8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tTA20satd2Ombos8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tTA20satd2Ombos8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tTA20satd2Ombos8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tTA20satd2Ombos8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tTA20satd2Ombos8 .marker.cross{stroke:#333333;}#mermaid-svg-tTA20satd2Ombos8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tTA20satd2Ombos8 p{margin:0;}#mermaid-svg-tTA20satd2Ombos8 g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-tTA20satd2Ombos8 g.classGroup text .title{font-weight:bolder;}#mermaid-svg-tTA20satd2Ombos8 .cluster-label text{fill:#333;}#mermaid-svg-tTA20satd2Ombos8 .cluster-label span{color:#333;}#mermaid-svg-tTA20satd2Ombos8 .cluster-label span p{background-color:transparent;}#mermaid-svg-tTA20satd2Ombos8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tTA20satd2Ombos8 .cluster text{fill:#333;}#mermaid-svg-tTA20satd2Ombos8 .cluster span{color:#333;}#mermaid-svg-tTA20satd2Ombos8 .nodeLabel,#mermaid-svg-tTA20satd2Ombos8 .edgeLabel{color:#131300;}#mermaid-svg-tTA20satd2Ombos8 .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-tTA20satd2Ombos8 .label text{fill:#131300;}#mermaid-svg-tTA20satd2Ombos8 .labelBkg{background:#ECECFF;}#mermaid-svg-tTA20satd2Ombos8 .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-tTA20satd2Ombos8 .classTitle{font-weight:bolder;}#mermaid-svg-tTA20satd2Ombos8 .node rect,#mermaid-svg-tTA20satd2Ombos8 .node circle,#mermaid-svg-tTA20satd2Ombos8 .node ellipse,#mermaid-svg-tTA20satd2Ombos8 .node polygon,#mermaid-svg-tTA20satd2Ombos8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tTA20satd2Ombos8 .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 g.clickable{cursor:pointer;}#mermaid-svg-tTA20satd2Ombos8 g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-tTA20satd2Ombos8 g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-tTA20satd2Ombos8 .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-tTA20satd2Ombos8 .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-tTA20satd2Ombos8 .dashed-line{stroke-dasharray:3;}#mermaid-svg-tTA20satd2Ombos8 .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-tTA20satd2Ombos8 #compositionStart,#mermaid-svg-tTA20satd2Ombos8 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #compositionEnd,#mermaid-svg-tTA20satd2Ombos8 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #dependencyStart,#mermaid-svg-tTA20satd2Ombos8 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #dependencyStart,#mermaid-svg-tTA20satd2Ombos8 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #extensionStart,#mermaid-svg-tTA20satd2Ombos8 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #extensionEnd,#mermaid-svg-tTA20satd2Ombos8 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #aggregationStart,#mermaid-svg-tTA20satd2Ombos8 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #aggregationEnd,#mermaid-svg-tTA20satd2Ombos8 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #lollipopStart,#mermaid-svg-tTA20satd2Ombos8 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 #lollipopEnd,#mermaid-svg-tTA20satd2Ombos8 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-tTA20satd2Ombos8 .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-tTA20satd2Ombos8 .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tTA20satd2Ombos8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tTA20satd2Ombos8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tTA20satd2Ombos8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ScheduleJobBootstrap\n作业启动入口
+schedule() : 启动作业
JobScheduler\n作业调度器
+init() : 初始化调度
SchedulerFacade\n调度门面
+getShardingContexts() : 获取本实例分片
+shutdownInstance() : 下线实例
<<interface>>
JobShardingStrategy\n分片策略抽象
+sharding() : 计算分片归属
平均分配策略
作业名奇偶策略
LiteJobFacade\n执行门面
+loadJobRootConfig() : 读取作业配置
+execute() : 执行分片业务
<<interface>>
ElasticJob\n作业接口
+execute(ShardingContext)
CoordinatorRegistryCenter\nZK 注册中心
+register() : 注册实例
+getChildrenKeys() : 监听节点
ElectionListenerManager\n主节点选举监听
ShardingListenerManager\n分片变更监听
FailoverListenerManager\n失效转移监听
6. 扩展点与常见问题
6.1 扩展点
| 框架 | 扩展点 | 方式 | 典型场景 |
|---|---|---|---|
| XXL-Job | 自定义路由策略 | 实现 ExecutorRouter |
机房亲和、按业务权重选执行器 |
| XXL-Job | 自定义告警 | 实现 JobAlarm |
钉钉 / 企微告警替代邮件 |
| XXL-Job | 自定义任务处理器 | 实现 IJobHandler 或 @XxlJob 注解 |
业务任务逻辑 |
| XXL-Job | GLUE 脚本 | 控制台在线编辑 | 免发版的一次性运维任务 |
| XXL-Job | 运维 API | /jobinfo/* 等 HTTP 接口 |
与 CI/CD、告警平台集成 |
| ElasticJob | 自定义分片策略 | 实现 JobShardingStrategy |
按机房 / 权重分片 |
| ElasticJob | 自定义作业类型 | 实现 ElasticJob 接口 |
特殊协议任务 |
| ElasticJob | 作业监听器 | ElasticJobListener / DistributeOnceElasticJobListener |
执行前后钩子、全局汇总 |
| ElasticJob | 事件追踪存储 | 自定义事件追踪数据源 | 审计、与监控平台打通 |
6.2 常见问题 FAQ
Q1:执行器(实例)宕机了,任务会丢吗?
XXL-Job:本次调度失败会告警 + 按配置重试,下一次调度可用故障转移/忙碌转移路由自动换节点;已触发未执行完的任务会因超时机制中断。ElasticJob:开启 failover 后,主节点协调把崩溃实例的分片自动转移给存活实例,几乎无感。
Q2:调度中心(ZK)宕机了影响什么?
XXL-Job:调度中心挂了任务全部停止触发 (执行器侧已触发的继续跑),因此生产必须部署多实例 + 共享 MySQL 做 HA。ElasticJob:ZK 挂不影响各实例本地 Quartz 照常执行,但重新分片、失效转移、实例上下线全部停摆,ZK 集群需自身保障可用性。
Q3:任务上次没跑完,这次调度又来了怎么办?
XXL-Job:靠 3 种阻塞处理策略(串行排队/丢弃/覆盖)控制;ElasticJob:开启 monitorExecution 后,错过调度时若上次仍在执行则跳过,避免并发重复。
Q4:分片任务怎么保证幂等?
分片只解决"并行",不解决"重复"。两层保障:① 框架层------XXL-Job 阻塞策略 + ElasticJob misfire 控制,避免同一片并发;② 业务层------按唯一键(订单号、状态机、乐观锁)幂等,兜底扩缩容引发的切片重排。
Q5:需要秒级甚至更高频调度怎么办?
两大框架底层都是 Quartz,cron 最细秒级 。秒级任务可用性尚可;高频海量小任务(每秒数千级触发)两者都不适合 ------XXL-Job 的调度中心会成为瓶颈,ElasticJob 的 ZK 协调开销过大,这类场景应自研时间轮(如 Netty HashedWheelTimer)或直接用消息队列延迟消费。
Q6:XXL-Job 多调度中心部署会重复触发吗?
不会。触发前 select for update 行锁预读任务行,同一调度时刻只有一个 Admin 实例拿到锁并触发,其余放弃。
Q7:GLUE 和 BEAN 怎么选?
长期、需要 Code Review 和测试的任务用 BEAN (代码内 @XxlJob);一次性、临时、运营自己维护的脚本用 GLUE。GLUE 多了要定期清理,防止执行器上代码腐化。
Q8:ElasticJob 为什么不适合海量小任务?
每个实例上下线、每次分片协商、每次执行状态上报都要写 ZK,任务量大时 ZK 写入压力大;且执行状态通过 ZK 传递,链路比 XXL-Job 直接回调重。ElasticJob 的最佳形态是任务少而重 (每日/每小时的批处理分片),XXL-Job 则更适合任务多而杂的业务线统一调度。
6.3 选型速查
| 场景 | 推荐 |
|---|---|
| 中小团队、多业务线、要开箱即用的控制台 | XXL-Job ⭐ |
| 数据量大、需要分片并行批处理 | ElasticJob(或 PowerJob) |
| 已有 ZooKeeper 运维体系,追求无单点 | ElasticJob |
| JDK 8 存量项目 | XXL-Job 2.4.x(3.x 版本线要求 JDK 17) |
| 高频海量小任务 | 自研时间轮 / 消息队列延迟消费 |
| 复杂编排 + 大数据量 MapReduce | PowerJob |
一句话概括 :要平台选 XXL-Job,要框架选 ElasticJob------中小团队、重视运维体验和排障效率选前者;重度分片弹性、追求无单点、已有 ZK 运维能力选后者。两者都能与 Spring Cloud 微服务无缝共存(执行器/作业以依赖形式内嵌服务),都不是注册中心绑定的组件。
7. 参考资料
- XXL-Job 官方仓库(最新开源版 v3.4.1,2026-06-14)
- XXL-Job v3.4.1 发布说明(Docker、XSS 防护、AI Ready 重构)
- Apache ShardingSphere ElasticJob 官方仓库(最新稳定版 3.0.5,2026-02-07)
- Apache ShardingSphere ElasticJob 官方文档