SpringCloud---Spring Cloud 分布式任务调度(XXL-Job / ElasticJob)

基于: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 单体定时任务的四个痛点)
    • [1.2 分布式任务调度的核心概念](#1.2 分布式任务调度的核心概念)
    • [1.3 主流方案定位表](#1.3 主流方案定位表)
    • [1.4 中心化 vs 去中心化两大流派](#1.4 中心化 vs 去中心化两大流派)
  2. [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 动态任务)
  3. [ElasticJob 设计思想与核心机制 ★★★](#ElasticJob 设计思想与核心机制 ★★★)
    • [3.1 去中心化与 ZooKeeper 协调](#3.1 去中心化与 ZooKeeper 协调)
    • [3.2 分片与弹性伸缩](#3.2 分片与弹性伸缩)
    • [3.3 作业类型与事件追踪](#3.3 作业类型与事件追踪)
  4. [主线流程剖析 ★★★](#主线流程剖析 ★★★)
    • [4.1 XXL-Job 一次调度的完整链路](#4.1 XXL-Job 一次调度的完整链路)
    • [4.2 ElasticJob 一次执行的完整链路](#4.2 ElasticJob 一次执行的完整链路)
    • [4.3 两大框架链路对比](#4.3 两大框架链路对比)
  5. 核心类关系图
    • [5.1 XXL-Job 核心类关系](#5.1 XXL-Job 核心类关系)
    • [5.2 ElasticJob 核心类关系](#5.2 ElasticJob 核心类关系)
  6. 扩展点与常见问题
    • [6.1 扩展点](#6.1 扩展点)
    • [6.2 常见问题 FAQ](#6.2 常见问题 FAQ)
    • [6.3 选型速查](#6.3 选型速查)
  7. 参考资料

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 任务有 BEANGLUE 两种模式:

模式 任务逻辑存放位置 修改成本 适用场景
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

串行执行任务
⑦ 回调调度中心

写日志 + 更新状态
⑧ 失败处理

按次数重试 + 告警

  1. Quartz 到点 :Admin 内嵌 Quartz,cron 到点后拿到 scheduleJobId,交给 JobTriggerPoolHelper,按任务"快/慢"属性选择线程池。
  2. 行锁预读XxlJobTrigger.processTriggerselect ... for update 预读任务行------集群多 Admin 场景下只有抢到行锁的实例继续,其余放弃,保证不重复触发。
  3. 选择执行器 :查 xxl_job_registry 拿到在线执行器地址列表,按任务配置的路由策略(默认轮询)选定目标地址;分片广播则取全部地址。
  4. RPC 下发 :通过 ExecutorBiz 动态代理(HTTP + Hessian)调用执行器内嵌 Jetty 暴露的 run() 接口。
  5. 执行器执行:执行器将触发入队到该任务专属 JobThread,串行执行;GLUE 模式则先取最新源码(Groovy 编译)再执行;注入分片参数、超时监控。
  6. 结果回调 :执行结果(成功/失败 + 日志)回调 Admin,写 xxl_job_log,更新任务状态。
  7. 失败处理 :按配置的重试次数自动重试;仍失败触发 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

执行分片业务
⑦ 上报状态

事件追踪
⑧ 实例异常

失效转移 + 重新分片

  1. 实例注册 :作业实例启动,向 ZK 注册 instances 临时节点,并拉取 config 节点下的作业配置。
  2. 主节点选举 :各实例竞争 leader/election,产生一个主节点,负责协调分片与失效转移。
  3. 分片计算 :主节点(或触发分片变更的实例)按配置的分片策略计算 N 个分片项的归属,写入 sharding 节点。
  4. 监听分片 :各实例监听 sharding 节点变化,取回属于自己的分片项(含 sharding-item-parameters 中的业务参数)。
  5. 本地触发 :cron 到点,每个实例的本地 Quartz 各自触发------没有远程调度调用,触发天然无延迟。
  6. 执行分片业务LiteJobFacade 组装 ShardingContext,调用业务实现的 execute()
  7. 状态上报:执行结果写入 ZK 执行状态节点,事件发布到事件追踪(RDB/日志)。
  8. 异常自愈:实例崩溃 → 临时节点消失 → 主节点协调失效转移,把其分片迁给存活实例;实例上线同理反向重平衡。

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. 参考资料

相关推荐
未秃头的程序猿5 小时前
一次秒杀把服务打挂了,我用Sentinel规则配置化救了回来
java·后端·spring cloud
天远Date Lab5 小时前
分布式微服务实战:基于天远车辆估值构建自动化车价评估网关
人工智能·分布式·微服务·自动化
随遇而安zx6 小时前
SpringCloud 分布式链路追踪 设计思想与源码深度解析
分布式·spring cloud
迪飞特科技7 小时前
分布式事务下 Spring 声明式事务失效的 3 种修复方案
分布式·spring·wpf
随遇而安zx7 小时前
Spring Cloud Gateway 微服务网关 设计思想与深度解析
java·spring cloud·微服务·架构
谢亮_vipxieliang7 小时前
ValidX vs Apache Commons Validator:功能与性能对比
java·服务器·spring boot·后端·spring cloud·apache·hibernate
这个DBA有点耶21 小时前
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
数据库·分布式·dba
谢亮_vipxieliang2 天前
ValidX分组验证详解:不同场景使用不同验证规则
java·spring boot·后端·spring cloud·eclipse·hibernate
AIGC小尼2 天前
2026 最新实战|SpringCloud 微服务整合 SpringAI 完整落地教程(RAG 知识库 + 限流降级 + 跨服务调用)
spring·spring cloud·微服务