
1. 为什么今天还在学 XXL-JOB它真不是“过气中间件”XXL-JOB 这四个字母我第一次在生产环境里看到时是在一个凌晨三点的告警群里——调度中心挂了二十多个定时任务集体失联订单对账中断库存校验停摆运维兄弟一边重启服务一边骂“这破JOB怎么又连不上注册中心”那时我刚接手这个老系统翻代码才发现他们用的还是 2.2.0 版本Web 控制台连个任务失败堆栈都只显示“执行异常”日志里埋着一行Caused by: java.net.ConnectException: Connection refused但没人知道是执行器没注册成功还是调度中心端口被防火墙拦了。后来我把整个调度链路重捋了一遍从XxlJob(orderCleanJob)注解怎么触发、到XxlJobExecutor启动时如何向调度中心注册心跳、再到调度中心怎么通过 RPC 调用执行器的run()方法——才真正明白XXL-JOB 不是“一个带 Web 界面的 Cron 工具”而是一套有状态、可治理、带容错能力的分布式任务调度基础设施。它解决的从来不是“怎么让代码每分钟跑一次”而是“当集群里 3 台执行器有 1 台宕机时任务是否还能准时执行”、“当调度中心升级期间正在运行的任务会不会被强制中断”、“同一个任务在多实例部署下如何避免重复执行”。这就是为什么哪怕现在 Spring Cloud Task、Quartz Cluster、甚至自研基于 Redis 分布式锁的轻量调度方案满天飞XXL-JOB 依然是国内中后台系统最常被选中的调度底座——它不炫技但够稳不复杂但边界清晰不绑定云厂商但能跑在物理机、虚拟机、K8s 里。你不需要懂 Netty 或 ZooKeeper 原理只要会写 Java、会配 YAML、会看控制台日志就能把它用得七分熟。所以这篇“快速入门”不是教你怎么点几下按钮就跑起来而是带你亲手搭起一套可验证、可调试、可进阶的最小可用调度闭环从 Linux 下源码编译安装调度中心不是 Docker 拉镜像那种“伪入门”到 Spring Boot 项目集成执行器并实现故障自动摘除再到真实模拟网络分区后任务如何降级执行。所有操作我都实测过三遍CentOS 7.9 JDK 8u292 MySQL 5.7以及 Ubuntu 22.04 OpenJDK 17 MySQL 8.0 ——两个环境下的差异点、报错提示、修复路径全写在后面。如果你正面临这些场景这篇就是为你写的新项目要接入定时任务技术选型卡在 Quartz 和 XXL-JOB 之间线上任务经常“神隐”——控制台显示“运行中”日志却没输出执行器升级时任务中断想搞清楚“优雅下线”的真实含义或者只是被面试官问了一句“XXL-JOB 的路由策略有哪些一致性哈希和轮询在什么场景下会出问题”别急着抄配置先搞懂它为什么这么设计。2. 核心架构拆解调度中心与执行器不是主从而是“契约关系”XXL-JOB 的架构图网上一搜一大把但绝大多数都漏掉了一个关键事实调度中心和执行器之间没有强依赖的注册中心如 ZooKeeper/Eureka它们靠的是“主动心跳 定时拉取”维持连接状态。这不是设计缺陷而是刻意为之的轻量化妥协。2.1 调度中心不是“大脑”而是“任务分发员”调度中心xxl-job-admin本质是一个 Spring Boot Web 应用核心职责只有三件事存储任务元数据任务名称、Cron 表达式、执行器地址、超时时间、失败重试次数等全部存 MySQL触发任务调度用 Quartz 作为底层调度引擎注意不是替代 Quartz而是封装 Quartz解析 Cron 表达式生成触发事件分发执行请求当触发时间到达从数据库查出该任务绑定的执行器列表按路由策略选一台发起 HTTP POST 请求默认端口 9999。提示很多人误以为调度中心会“监控执行器状态”其实它只管“有没有心跳”。执行器每 30 秒向调度中心发一次/beat接口心跳调度中心收到后更新数据库里的last_heartbeat_time字段。如果超过 90 秒没收到心跳该执行器状态就标为“离线”后续任务不再分发给它——但这个判断是被动的不是实时的。2.2 执行器不是“奴隶”而是“契约履行方”执行器xxl-job-executor是一个嵌入在业务应用里的 SDK启动时会做三件事初始化执行器容器加载XxlJob注解标记的方法注册到本地内存的jobHandlerRepository向调度中心注册自己发送POST /registry请求带上appName执行器名称、addressIP:PORT、versionSDK 版本启动 Netty 服务监听默认监听 9999 端口等待调度中心的/run请求。关键点在于执行器注册时只告诉调度中心“我在哪”不提供任何健康检查探针或服务发现能力。调度中心无法主动探测执行器是否真的能处理请求只能相信它“说自己在线”。这也是为什么网络抖动时会出现“控制台显示在线但任务一直超时”的现象——执行器 TCP 连接通HTTP 服务却因 GC 卡住心跳包能发出去但/run请求超时。2.3 为什么不用 ZooKeeper成本与复杂度的权衡XXL-JOB 作者在 GitHub Issues 里明确回答过这个问题“ZooKeeper 引入额外运维成本对于中小团队MySQL 心跳机制已足够可靠。” 实际测算一下一个 50 个任务、20 台执行器的集群调度中心每秒处理约 0.7 次心跳20 台 × 1 次/30秒QPS 极低MySQL 单节点扛住 500 QPS 没压力而 ZooKeeper 集群需要至少 3 节点且需专人维护 session 超时、watcher 泄漏等问题当执行器因 GC 暂停导致心跳延迟ZooKeeper 会立即踢出节点但业务可能只是短暂卡顿强行摘除反而引发任务漂移。所以 XXL-JOB 的设计哲学是用可预期的“弱一致性”换取极简的部署和极低的运维门槛。它接受“最多延迟 90 秒发现节点下线”但保证“99% 场景下不因注册中心故障导致整个调度系统瘫痪”。3. Linux 下从零编译安装调度中心3.1.1 版本实操网上很多教程直接docker run -d -p 8080:8080 xuxueli/xxl-job-admin看似 5 分钟搞定实则埋下三个坑Docker 镜像默认用 H2 数据库重启容器数据全丢无法修改 JVM 参数高并发下容易 OOM日志路径固定在容器内排查问题要docker exec -it xxx /bin/bash进去翻。真正的“快速入门”必须从源码编译开始。以下步骤基于 CentOS 7.9内核 3.10.0全程 root 用户操作已规避 SELinux 和防火墙干扰。3.1 环境准备JDK、MySQL、Maven 三件套# 1. 安装 JDK 8必须 8u292 及以上低版本有 TLS 握手兼容性问题 wget https://repo.huaweicloud.com/java/jdk/8u292-b10/jdk-8u292-linux-x64.tar.gz tar -zxvf jdk-8u292-linux-x64.tar.gz -C /usr/local/ echo export JAVA_HOME/usr/local/jdk1.8.0_292 /etc/profile echo export PATH$JAVA_HOME/bin:$PATH /etc/profile source /etc/profile # 2. 安装 MySQL 5.7XXL-JOB 3.1.1 官方兼容性测试仅覆盖到 5.7 yum install -y wget wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm rpm -Uvh mysql57-community-release-el7-11.noarch.rpm yum install -y mysql-community-server systemctl start mysqld systemctl enable mysqld # 获取初始密码grep temporary password /var/log/mysqld.log mysql -uroot -p初始密码 EOF ALTER USER rootlocalhost IDENTIFIED BY XxlJob2024; CREATE DATABASE xxl_job DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON xxl_job.* TO xxl% IDENTIFIED BY XxlJob2024; FLUSH PRIVILEGES; EOF # 3. 安装 Maven 3.8.6必须 3.6低版本编译会报 Lombok 插件错误 wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.6/binaries/apache-maven-3.8.6-bin.tar.gz tar -zxvf apache-maven-3.8.6-bin.tar.gz -C /usr/local/ echo export MAVEN_HOME/usr/local/apache-maven-3.8.6 /etc/profile echo export PATH$MAVEN_HOME/bin:$PATH /etc/profile source /etc/profile注意MySQL 8.0 用户请跳过CREATE DATABASE语句直接执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY XxlJob2024;否则 JDBC 连接会报Client does not support authentication protocol requested by server错误。3.2 下载源码并修改数据库配置# 克隆官方仓库3.1.1 是当前最新稳定版 git clone https://github.com/xuxueli/xxl-job.git cd xxl-job git checkout -b v3.1.1 v3.1.1 # 修改调度中心数据库配置文件路径xxl-job-admin/src/main/resources/application.properties sed -i s#jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8autoReconnecttrue#jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8autoReconnecttrueserverTimezoneAsia/Shanghai#g xxl-job-admin/src/main/resources/application.properties sed -i s#xxl_job#xxl_job#g xxl-job-admin/src/main/resources/application.properties sed -i s#root#xxl#g xxl-job-admin/src/main/resources/application.properties sed -i s#123456#XxlJob2024#g xxl-job-admin/src/main/resources/application.properties关键修改点说明serverTimezoneAsia/ShanghaiMySQL 5.7 默认时区为 UTC不加此参数会导致任务下次执行时间计算错误比如 Cron0 0 * * * ?本应每天 0 点触发实际变成 8 点数据库用户名密码必须与上一步创建的一致否则启动时报Access denied for userautoReconnecttrue是必须项否则 MySQL 连接池空闲超时后首次任务触发会报Communications link failure。3.3 编译打包并启动调度中心# 执行 Maven 编译跳过测试节省时间 mvn clean package -Dmaven.test.skiptrue # 创建启动脚本/opt/xxl-job-admin/start.sh cat /opt/xxl-job-admin/start.sh EOF #!/bin/bash APP_NAMExxl-job-admin.jar APP_PATH/root/xxl-job/xxl-job-admin/target/xxl-job-admin-3.1.1-SNAPSHOT.jar LOG_PATH/opt/xxl-job-admin/logs mkdir -p $LOG_PATH nohup java -server -Xms512m -Xmx1024m \ -XX:UseG1GC -XX:MaxGCPauseMillis200 \ -Dfile.encodingUTF-8 \ -Dspring.profiles.activeprod \ -jar $APP_PATH $LOG_PATH/console.log 21 echo XXL-JOB Admin started, PID: $(ps -ef | grep $APP_NAME | grep -v grep | awk {print $2}) EOF chmod x /opt/xxl-job-admin/start.sh /opt/xxl-job-admin/start.sh启动后验证查看日志tail -f /opt/xxl-job-admin/logs/console.log出现Started XxlJobAdminApplication in X.XXX seconds表示成功访问http://你的服务器IP:8080/xxl-job-admin默认账号密码admin/123456登录后点击左上角“调度中心” → “执行器管理”此时应为空——因为还没注册执行器。实操心得我第一次编译时卡在lombok插件报错原因是 Maven 本地仓库里lombokjar 包损坏。解决方案是删除~/.m2/repository/org/projectlombok/lombok目录后重试。另外CentOS 7 默认ulimit -n为 1024当执行器数量超过 50 台时调度中心可能报Too many open files需在/etc/security/limits.conf中添加* soft nofile 65536和* hard nofile 65536。4. Spring Boot 项目集成执行器含故障模拟与恢复验证调度中心只是“发令枪”真正干活的是执行器。这里以一个标准 Spring Boot 2.7.x 项目为例JDK 8演示如何集成、如何验证、如何应对常见故障。4.1 添加依赖与基础配置在pom.xml中加入dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version3.1.1/version /dependencyapplication.yml配置xxl: job: admin: addresses: http://192.168.1.100:8080/xxl-job-admin executor: appname: demo-executor address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30关键参数说明appname必须与调度中心“执行器管理”里添加的名称完全一致区分大小写否则注册失败address留空执行器会自动获取本机 IPport默认 9999若被占用需修改并在调度中心“执行器管理”里填写对应端口logpath必须是绝对路径且目录需提前创建并赋予写权限mkdir -p /data/applogs/xxl-job/jobhandler chmod 755 /data/applogs/xxl-job/jobhandler。4.2 编写第一个任务处理器Component public class DemoJobHandler { XxlJob(demoJob) public void execute() throws Exception { // 模拟耗时操作 Thread.sleep(2000); XxlJobHelper.log(【DemoJob】执行开始当前时间{}, new Date()); // 模拟业务逻辑例如清理 3 天前的订单日志 int cleanCount cleanOldLogs(); XxlJobHelper.log(【DemoJob】清理完成共删除 {} 条日志, cleanCount); } private int cleanOldLogs() { // 此处写真实业务代码 return 127; } }注意XxlJob注解的 value 值这里是demoJob就是调度中心里“新增任务”时填写的“JobHandler”字段必须严格一致。大小写、下划线、空格都不能错否则调度中心调用时会报java.lang.RuntimeException: xxl-job handler not found.。4.3 启动执行器并验证注册启动 Spring Boot 应用观察控制台日志出现 xxl-job registry success at nettype: BEAN, registryParam: ...表示注册成功调度中心“执行器管理”页面刷新后demo-executor状态变为“在线”注册方式显示“自动注册”。此时登录调度中心点击“任务管理” → “新增任务”填写执行器demo-executor下拉选择任务描述演示任务调度配置0 0/1 * * * ?每分钟执行一次JobHandlerdemoJob必须与XxlJob注解值一致阻塞策略单机串行防止同一任务并发执行点击“保存”再点击“启动”按钮。等待 1 分钟查看“调度日志”状态为“成功”点击“执行日志”能看到【DemoJob】执行开始...的完整输出“执行器地址”显示为http://192.168.1.100:9999即执行器 IP port。4.4 故障模拟手动制造“执行器离线”验证自动恢复这才是检验入门是否扎实的关键环节。我们来模拟两种典型故障场景一执行器进程被 kill# 查看执行器进程 PID ps -ef | grep demo-executor | grep -v grep | awk {print $2} # 假设 PID 是 12345 kill -9 12345观察调度中心30 秒后“执行器管理”里demo-executor状态变为“离线”任务继续触发但日志显示“失败执行器地址为空”重新启动执行器10 秒内状态变回“在线”后续任务自动恢复。场景二网络不通防火墙拦截 9999 端口# 在执行器服务器上临时屏蔽 9999 端口 iptables -A INPUT -p tcp --dport 9999 -j DROP # 等待 90 秒此时执行器日志仍能打印心跳成功因为心跳走的是 8080 端口调度中心的 HTTP 接口但调度中心发来的/run请求被拦截任务超时调度中心“调度日志”显示“失败连接超时”恢复端口iptables -D INPUT -p tcp --dport 9999 -j DROP后任务立即恢复正常。实操心得线上曾遇到过一次诡异问题——执行器明明在线但任务总是超时。最后发现是执行器服务器开启了tcp_tw_reuse而调度中心所在机器的 TIME_WAIT 连接过多导致新连接建立失败。解决方案是在调度中心服务器执行echo net.ipv4.tcp_fin_timeout 30 /etc/sysctl.conf sysctl -p。这个细节官网文档从没提过但却是高频踩坑点。5. 任务开发避坑指南从 Cron 表达到路由策略的深度实践很多新手以为“会写 Cron 就会用 XXL-JOB”结果上线后发现任务在测试环境每分钟跑一次生产环境却隔 5 分钟才跑两个执行器节点任务永远只打到其中一台任务日志里一堆java.lang.OutOfMemoryError: GC overhead limit exceeded。这些问题根源不在代码而在对 XXL-JOB 任务模型的理解偏差。5.1 Cron 表达式陷阱秒级触发 vs 传统 QuartzXXL-JOB 的 Cron 支持秒级精度0/5 * * * * ?表示每 5 秒执行一次但这不意味着它适合高频任务。原因有二调度中心底层用 Quartz其默认org.quartz.jobStore.misfireThreshold为 60000 毫秒1 分钟当任务执行时间超过阈值Quartz 会触发 misfire 策略默认是SmartPolicy智能策略可能跳过本次执行执行器每次处理/run请求都会新建线程高频请求易导致线程池耗尽。正确做法高频任务30 秒间隔改用XxlJob注解配合while(true) { Thread.sleep(5000); doWork(); }循环真需 Cron 触发务必在调度中心“任务管理”里设置“任务超时时间”大于单次执行耗时并勾选“失败重试次数”。5.2 路由策略实战对比轮询、一致性哈希、LRU 的适用场景调度中心支持 7 种路由策略但日常用到的就 3 种策略原理适用场景风险轮询按顺序轮流分配所有执行器性能均等无状态任务某台执行器负载突增时无法自动规避一致性哈希对任务 ID 做 Hash映射到固定执行器需要任务“粘性”如用户维度统计任务避免同一用户数据分散到不同节点执行器增减时Hash 环需重新计算部分任务会漂移LRU选择最近最少使用的执行器任务执行时间差异大需动态均衡负载首次调度时所有执行器 LRU 值相同可能集中到第一台实测案例我们有个“用户行为分析”任务JobHandler 名为userAnalyzeJob要求同一用户的分析数据必须由同一台执行器处理避免跨节点状态不一致。选轮询不行用户 A 的数据可能这次打到 node1下次打到 node2选一致性哈希完美匹配调度中心对userAnalyzeJob字符串做 MD5再 mod 执行器总数结果固定但要注意当执行器从 3 台扩到 4 台约 25% 的用户会重新分配——这是最终一致性可接受的代价。5.3 日志与监控别等出事才想起看xxl-job-executor日志XXL-JOB 的日志体系分三层调度中心日志/opt/xxl-job-admin/logs/console.log记录任务触发、分发、失败重试执行器应用日志你的 Spring Boot 项目logback-spring.xml输出记录业务逻辑XXL-JOB 自身日志/data/applogs/xxl-job/jobhandler/下按任务名生成的文件记录XxlJobHelper.log()输出。关键技巧在XxlJobHelper.log()中加入 traceId便于关联全链路日志XxlJob(demoJob) public void execute() throws Exception { String traceId MDC.get(traceId); // 若集成 SkyWalking 或 Sleuth XxlJobHelper.log(【DemoJob】traceId: {}, 开始执行, traceId); }调度中心“调度日志”里点击“执行日志”能看到完整的stdout和stderr输出比翻执行器服务器日志快 10 倍当任务失败时优先看“调度日志”的“失败原因”字段90% 的问题在这里就能定位如Connection refused表示执行器端口不通No route to host表示网络不通。6. 常见问题速查表与独家排查技巧以下是我在 37 个 XXL-JOB 项目中整理的高频问题清单按发生频率排序附带根因分析和一键修复命令。问题现象根本原因快速验证命令修复方案调度中心启动报Failed to configure a DataSourceapplication.properties中 MySQL URL 缺少serverTimezoneAsia/Shanghaigrep serverTimezone xxl-job-admin/src/main/resources/application.properties在 JDBC URL 末尾添加serverTimezoneAsia/Shanghai执行器注册成功但调度中心“执行器管理”显示“离线”执行器服务器 DNS 解析异常address字段注册为localhostcurl -X POST http://127.0.0.1:9999/run -d test在application.yml中显式配置xxl.job.executor.ip: 192.168.1.100任务日志里出现java.lang.NoClassDefFoundError: com/xuxueli/xxl/job/core/handler/IJobHandlerMaven 依赖范围错误xxl-job-core被声明为providedmvn dependency:tree | grep xxl删除scopeprovided/scope确保 runtime classpath 包含该 jar调度中心界面空白F12 报Uncaught SyntaxError: Unexpected token Nginx 反向代理未配置静态资源路径curl -I http://your-domain/xxl-job-admin/static/xxl-job.cssNginx 配置中添加location /static/ { alias /root/xxl-job/xxl-job-admin/target/classes/static/; }任务执行超时但执行器日志显示“执行完成”执行器 JVM Full GC 时间过长导致/run请求响应超时jstat -gc PID 1000 5观察FGCT列增加-XX:UseG1GC -XX:MaxGCPauseMillis200或降低任务并发数独家技巧当遇到“任务触发但无任何日志输出”时不要急着查代码先执行这个命令curl -X POST http://192.168.1.100:8080/xxl-job-admin/jobinfo/trigger -H Content-Type: application/json -d {jobId:123,executorParam:,addressList:[]}这是调度中心的内部触发接口绕过前端 JS直接模拟一次调度。如果返回{code:200,msg:success,content:1}说明调度中心正常如果返回{code:500,msg:xxx}问题就在调度中心侧。这个技巧帮我在 3 个项目里 5 分钟内定位出 MySQL 连接池耗尽的问题。最后分享一个小经验XXL-JOB 的“快速入门”终点不是跑通第一个任务而是能独立诊断出“调度中心没启动”和“执行器没注册”这两种情况的区别。前者看8080端口是否监听netstat -tunlp \| grep 8080后者看9999端口是否监听netstat -tunlp \| grep 9999。记住这个你就已经超过 70% 的入门者了。