XXL-Job架构设计原理
一、整体架构概述
flowchart TB
%% ========== 调度中心 Admin ==========
subgraph XXLJOBADMIN["XXL-JOB-ADMIN【调度中心 - 集群,共享MySQL】"]
DB[("MySQL xxl_job_info / xxl_job_log")]
ST["scheduleThread 预读线程<br/>循环扫描:trigger_next_time ≤ now+5s<br/>计算cron下一次触发时间 → 更新DB trigger_next_time"]
RW["ringData【单层60格时间轮】<br/>0~59秒slot,仅缓存5s窗口内待调度jobId"]
RT["ringThread 时间轮驱动线程<br/>每秒执行,取出当前刻度jobId"]
subgraph TriggerPool["下发触发线程池(快慢池)"]
FP["fastPool 快池 max=200"]
SP["slowPool 慢池 max=100"]
end
ST -->|"加载jobId到对应秒slot"| RW
RW --> RT
RT -->|"分发调度任务"| FP
RT -->|"分发调度任务"| SP
end
%% ========== 网络IO:HTTP推模式 ==========
FP -->|"HTTP POST /run 推送调度指令"| NETTY["执行器内嵌Netty HttpServer<br/>IO模型:Netty Reactor主从多线程<br/>worker线程:只接收&校验,不执行业务"]
SP -->|"HTTP POST /run 推送调度指令"| NETTY
%% ========== 执行器 Executor(和业务同JVM) ==========
subgraph XXLJOBEXECUTOR["XXL-JOB-EXECUTOR【执行器,嵌入业务进程】"]
EB["ExecutorBiz 业务门面<br/>校验token、jobId、handler"]
JTR["jobThreadRepository<br/>Map(jobId, JobThread) 每个jobId独立JobThread"]
subgraph JobThreadModel["JobThread模型【核心!单jobId=1线程 + 无界队列】"]
JT1["JobThread jobId=1<br/>死循环 take triggerQueue<br/>串行消费"]
Q1["triggerQueue 无界LinkedBlockingQueue"]
JT2["JobThread jobId=2<br/>死循环 take triggerQueue"]
Q2["triggerQueue 无界LinkedBlockingQueue"]
end
HANDLER["@XxlJob JobHandler<br/>用户业务代码"]
BRANCH{"阻塞策略判断"}
REJ["直接拒绝,返回失败"]
INT["interrupt当前任务+清空队列"]
BRANCH -->|"单机串行"| Q1
BRANCH -->|"丢弃后续调度"| REJ
BRANCH -->|"覆盖之前调度"| INT
Q1 --> JT1
Q2 --> JT2
JT1 --> HANDLER
JT2 --> HANDLER
end
%% ========== 回调链路 ==========
HANDLER -->|"执行完成/异常"| CB["回调线程<br/>异步HTTP上报结果+日志给Admin"]
CB --> DB
%% 主链路
NETTY --> EB
EB --> BRANCH
%% 自定义备注框,支持样式+虚线
NoteA["📌 调度模型要点<br/>单层60s时间轮,不是多层HashedWheelTimer<br/>超过5s窗口任务放DB,靠scheduleThread预读加载"]:::note
ST -.- NoteA
NoteB["📌 IO模型<br/>Admin:同步HTTP推<br/>Executor:Netty NIO接收调度请求,立刻返回签收<br/>业务异步在JobThread跑"]:::note
NETTY -.- NoteB
NoteC["📌 隔离特点<br/>JobThread按jobId隔离,不同任务互不阻塞<br/>闲置JobThread自动回收"]:::note
JTR -.- NoteC
classDef note fill:#fffde7,stroke:#f9a825,stroke-width:1px
XXL-JOB 采用 中心化调度 + 执行器推送模型,分为两大关键组件:
- 调度中心(Admin):负责任务CRUD、cron时间计算、定时触发、下发调度指令、接收执行结果、日志存储;支持集群部署,依赖MySQL做分布式协调。
- 执行器(Executor):嵌入业务JVM进程,接收调度中心HTTP指令,执行业务定时逻辑,执行完成后异步上报结果。
Push模式:调度中心主动推指令给执行器。
二、调度中心:调度模型 & 线程设计
2.1 单层60秒时间轮(核心定时机制)
重点:不是Netty那种多层HashedWheelTimer
ringData:一共60个slot,对应0~59秒,仅缓存未来5s窗口内待调度jobId;- 长周期(小时/天级)任务不会常驻时间轮,任务触发时间持久化在MySQL
xxl_job_info.trigger_next_time; - 分工两个核心线程:
scheduleThread(预读线程):循环扫描数据库,筛选trigger_next_time <= now + 5s的任务;使用Cron表达式算出下一次触发时间,更新数据库预生成下次触发时间,然后把jobId放入对应秒刻度的时间轮。ringThread(时间轮驱动线程,单线程):每秒执行一次,取出当前时间刻度的job,交给触发线程池下发。>
设计取舍:放弃多层时间轮,用「DB持久化 + 短窗口单层时间轮」,实现简单、避免大量任务常驻内存,适合Cron类定时任务;局限:仅秒级精度,不支持毫秒调度。
2.2 快慢触发线程池 TriggerPool
ringThread 是单线程,绝对不能阻塞在远程HTTP调用上,否则整个调度停摆,所以HTTP下发动作交给独立线程池,并且拆分为 fastPool / slowPool:
- 判定规则:统计本次HTTP下发签收耗时(不是业务执行耗时),小于500ms判定为快任务,否则标记慢任务;状态动态漂移。
- fastPool 默认上限200:承载绝大多数网络正常的调度下发;
- slowPool 默认上限100:专门隔离网络卡顿、跨机房的执行器调用。
- 设计目的:故障隔离。少量慢执行器不会耗尽全部线程资源,保护正常任务调度不被拖垮;slow池耗尽时,该次调度直接失败,不影响fast池。
误区:快慢池控制的是【调度下发HTTP请求】,和
@XxlJob业务代码执行时长无关。哪怕业务跑1小时,只要执行器快速返回签收响应,依然是快任务。
三、执行器:IO模型 & JobThread 线程模型
3.1 IO模型:Netty NIO 推模式
执行器内置 Netty HTTP Server(老版本Jetty),Reactor主从多线程模型:
- Netty worker 线程负责网络IO、报文解析、token、handler合法性校验;
- 收到
/run请求,校验通过后,立刻返回HTTP响应给调度中心;>
HTTP返回成功 = 仅代表【执行器成功收下调度指令】,不代表业务执行成功;业务在后台异步执行。
- Netty IO线程禁止执行业务代码,防止业务阻塞卡死IO接收链路。
3.2 JobThread 线程模型
执行器内部不是全局共享线程池,粒度为 jobId:
jobThreadRepository:Map<Integer, JobThread>,key=jobId;- 每个jobId首次调度时,创建独立
JobThread+ 专属无界阻塞队列triggerQueue; - JobThread内部死循环
take()自己队列的调度记录,串行消费该job所有调度; - 隔离效果:A任务死循环阻塞,不会影响其他jobId的JobThread;
- 回收机制:队列为空、持续空闲达到阈值,自动销毁JobThread,释放线程资源。
3.3 阻塞策略(队列处理策略)
当新调度到达、该job已有任务正在执行时,三种策略控制入队行为:
- 单机串行(默认):调度放入无界队列排队;风险:任务执行周期小于调度频率时,队列无限堆积,内存持续上涨;
- 丢弃后续调度:拒绝本次新调度,直接返回失败,不入队;
- 覆盖之前调度:中断当前正在运行的任务、清空队列,直接执行本次新调度。
四、结果回调链路
- JobThread 执行业务
@XxlJobHandler,正常结束或者抛出异常; - 启动独立回调线程,异步发起HTTP请求上报最终执行状态、日志、返回信息给调度中心;
- 调度中心收到回调,写入
xxl_job_log,更新任务执行结果。
分离“调度下发响应”和“最终结果回调”,是异步调度的核心设计:调度签收和业务结果解耦。
五、分布式防重复调度
调度中心集群共用MySQL,依靠 xxl_job_info.trigger_next_time + 行锁保证同一个任务同一时刻只会被一个admin节点调度下发,避免集群重复调度。
- select 拿到一批满足 trigger_next_time <= now+5s 的job列表
- for循环遍历每个job,逐个执行update抢占
UPDATE xxl_job_info
SET trigger_next_time = #{nextTriggerTime},
trigger_last_time = #{oldTriggerNextTime}
WHERE id = #{jobId}
AND trigger_next_time = #{oldTriggerNextTime};
六、架构优势总结
- 接入成本低:执行器嵌入业务进程,引入依赖 + 注解即可接入,不需要单独部署Agent;
- 任务天然隔离:jobId粒度独立JobThread,不同定时任务互相不阻塞;
- 故障隔离能力:快慢池隔离慢执行器,防止单点网络问题全局影响调度;
- Cron调度友好:预计算下次触发时间存入DB,不需要每次调度全量Cron匹配,任务量大时CPU可控;
七、架构缺陷 & 风险
- 执行器和业务同JVM,资源不隔离
- 业务FullGC、死锁、线程打满 → Netty IO线程阻塞,收不到调度指令;
- 定时任务OOM、死循环,直接拖垮主业务服务。
- 优化方案:独立执行器进程,业务逻辑通过RPC/MQ下沉。
- 单机串行+无界队列风险 默认策略下,任务执行耗时 > 调度周期,队列持续堆积,内存溢出。
- 秒级调度精度上限,原生不支持毫秒级定时;
- 依赖MySQL做分布式协调,大集群高调度QPS下,DB存在压力瓶颈。