先交代一下背景。我手上有个老项目,定时任务一直用的是 Spring 自带的@Scheduled,代码里一坨一坨的@Component加注解方法,部署了四个实例之后问题开始集中爆发:同一个任务在凌晨同时被四台机器执行,某些不幂等的业务逻辑直接算错;某天一台实例内存溢出重启,结果第二天用户投诉才知道整条任务链断了。后来我把这套调度体系整体迁移到 XXL-Job,从调度中心部署、执行器接入到分片任务调优,踩了不少坑,也把整个链路彻底摸通了。这篇文章就把 Spring Boot 整合 XXL-Job 的完整路径梳理一遍,包括每一步怎么操作、每个配置为什么这么写,以及那些网上不太有人讲清楚的故障排查思路,给正在折腾这块的兄弟们一个参考。
1. 单机定时任务撑不住的几个场景:为什么要引入 XXL-Job
1.1 多实例部署下的重复执行问题
很多人一开始接触@Scheduled的时候都觉得这个注解真香,一个方法加个 cron 就完事,甚至比 Quartz 还要简单。但在分布式环境下,@Scheduled是没有分布式锁概念的,应用部署几个副本,任务就会在几个副本上同时执行。如果任务本身是幂等的,比如把某个缓存刷新一下,那重复执行影响不大;可一旦遇到"定时发送短信""定时对账""生成报表并推送"这类有副作用的逻辑,重复执行就是事故现场。
网上应对这个问题的常规套路是用 Redis 分布式锁,或者数据库里的select for update悲观锁来保证同一时刻只有一个实例在跑。听起来可行,但实际落地会发现,每加一个定时任务都得写一套加锁解锁逻辑,而且锁的过期时间、宕机后的锁释放也都是坑。任务少的时候还能忍,任务一多,代码里全是和业务无关的锁逻辑,维护成本直线上升。
1.2 没有统一的任务管理视图
用@Scheduled的第二个痛点,是任务状态完全黑盒。系统里到底有多少定时任务?每个任务上次运行是什么时候?耗时多少?成功还是失败?这些问题没有一个统一的地方能看到。你只能去翻服务日志,用grep去捞每个任务的执行痕迹,非常痛苦。
更麻烦的是动态调整执行频率。业务方说"这个报表能不能改成半小时出一次",你只能改代码里的 cron 表达式,然后重新打包、发布、重启服务。整个过程少说半小时,而且还有发布窗口的限制。想手动触发一次任务来看看效果?@Scheduled根本做不到,只能等下一次调度。
1.3 缺失败重试和告警
第三个问题是最致命的:任务挂了之后没有人知道。@Scheduled方法里抛异常,默认结果就是控制台打一段堆栈,然后这个任务的下一次执行要等到下一个周期。如果是一个凌晨跑批的任务,半夜两点执行失败,你早上十点上班前根本不会注意到,直到业务方反馈"今天的账单数据怎么没生成",你才知道出了问题。对很多对时效性敏感的业务来说,这种体验绝对是灾难。
当然,@Scheduled本身并非不能用,如果你的项目是单机部署、任务数量很少、对失败容忍度高,它确实是最省事的选择。但一旦上了多实例、任务数量和业务敏感性上来,一个真正的分布式任务调度平台就是刚需。
1.4 XXL-Job 的核心设计理念
XXL-Job 是国内用的比较多的开源分布式任务调度平台,它的核心设计是"调度中心"和"执行器"分离。调度中心是一个独立部署的 Web 应用,负责任务的创建、编排、触发、日志存储和告警通知;执行器则是嵌入到你的 Spring Boot 应用里的一个轻量级组件,负责接收调度中心的指令并执行业务逻辑。两者之间通过 HTTP 接口通信。
简单类比:调度中心是导演,执行器是演员。导演负责决定什么时候开拍、拍哪场戏,演员接到指令后执行表演,再把结果回报给导演。这样业务系统不需要关心"任务什么时候触发""失败了怎么重试"这些事,只需要专注于"这个任务具体要做什么逻辑"。
| 对比维度 | @Scheduled | XXL-Job |
|---|---|---|
| 多实例执行 | 每个实例都会执行 | 通过路由策略控制单次/分片执行 |
| 任务管理界面 | 无 | 有完整的后台管理界面 |
| 动态调整 cron | 改代码发版 | 界面直接修改,实时生效 |
| 失败重试 | 手动实现 | 内置失败重试次数配置 |
| 告警通知 | 无 | 内置邮件告警 |
| 手动触发任务 | 不支持 | 后台一键执行 |
| 日志查看 | 翻服务日志 | 调度中心在线查看执行日志 |
2. 调度中心先行:XXL-Job Admin 的部署与初始化
2.1 下载源码包与初始化数据库
XXL-Job 的调度中心本身也是一个 Spring Boot 应用,叫xxl-job-admin。我建议从 Gitee 的官方仓库(xuxueli/xxl-job)拉取 release 版本的源码包,我当时用的是 2.4.0。解压之后你会看到两个核心目录:xxl-job-admin是调度中心,xxl-job-core是后面我们要引入到业务项目里的核心依赖。
初始化数据库这一步是最容易被忽略的。XXL-Job 的所有任务配置、调度记录、执行器信息都存储在数据库里,官方在源码目录下提供了一个建表脚本:xxl-job/doc/db/tables_xxl_job.sql。我们需要先在 MySQL 里创建一个专用库,比如xxl_job,然后把脚本执行进去:
mysql -uroot -p -e "CREATE DATABASE xxl_job DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p xxl_job < tables_xxl_job.sql脚本执行完之后,库里会出现多张表,比较关键的有:
xxl_job_info:任务信息表,存的是每个任务的基本配置。xxl_job_log:调度日志表,每个任务每次触发的记录都在这里。xxl_job_registry:执行器注册表,在线执行器的心跳信息在这个表里能看到。xxl_job_group:执行器分组表。
2.2 修改配置并启动 Admin 服务
数据库初始化好之后,进入xxl-job-admin模块,打开src/main/resources/application.properties文件,核心配置项就这么几处:
server.port=8080 server.servlet.context-path=/xxl-job-admin spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver xxl.job.accessToken=default_token这里有两个地方要特别说一下。
第一个是context-path必须保留/xxl-job-admin,因为 Admin 的前端页面和接口路径都基于这个上下文,你要是给它改掉,后面执行器回调的地址匹配会出问题。
第二个是xxl.job.accessToken,这是调度中心和执行器之间的通信令牌,两边必须配置一致,否则调度中心发出的请求会被执行器拒绝。我习惯先设成一个固定值,比如default_token,后面引入执行器时保持一致。如果你想做更严格一点,用 UUID 或一串随机字符串,本地演示没必要。
还有两个可选配置:spring.mail.host、spring.mail.username等邮件配置,用于失败告警,本地调试可以先不配,等上线前再补上。
配置完成后,在xxl-job-admin目录下执行 Maven 打包:
mvn clean package -DskipTests打包产物在xxl-job-admin/target/xxl-job-admin-2.4.0.jar,直接java -jar启动:
java -jar xxl-job-admin-2.4.0.jar启动完成后浏览器访问http://localhost:8080/xxl-job-admin,默认账号密码是admin / 123456。能看到登录页说明调度中心已经跑起来了。
2.3 Admin 后台必须熟悉的四大模块
登录进去之后,左侧菜单里有几块是日常使用频率极高的,建议先花几分钟过一遍。
执行器管理:这里维护的是所有接入调度中心的业务应用。执行器以 appname 为唯一标识,一个 Spring Boot 服务对应一个执行器。我们在后面接入业务项目时,第一步就是在这里新增一条执行器记录。
任务管理:所有定时任务的入口。创建任务、修改 cron、配置路由策略、手动触发、查看执行日志都从这里进。任务页面的字段比较多,后面我会逐个展开讲。
调度日志:每个任务每次调度的完整记录,包括调度结果、执行结果、执行耗时、执行的机器地址。排查任务不动、执行失败的问题,基本都靠这个页面。
用户管理:XXL-Job 自带了一个简单的权限体系,可以给团队成员分配不同的角色和权限。默认的 admin 账号是全权限,生产环境建议创建只读账号给业务方查看任务状态用,避免误操作。
3. Spring Boot 项目接入执行器:从依赖到自动注册
3.1 Maven 依赖引入与版本选择
调度中心部署好之后,接下来就是把我们的 Spring Boot 业务项目变成"执行器"。这一步的核心是引入xxl-job-core依赖。
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.4.0</version> </dependency>版本选择上有个值得注意的点。如果你用的是 Spring Boot 2.6 以下版本,用 2.3.1 这类老版本问题不大;但如果你跟我一样用 Spring Boot 2.7.x,甚至是 Spring Boot 3.x,建议直接上 2.4.0 或更新版本。原因是 Spring Boot 2.6 之后默认禁止了 Bean 的循环依赖,而 XXL-Job 老版本内部存在循环引用,启动时会直接报错。2.4.0 版本对这块做了适配,能减少很多不必要的麻烦。
| Spring Boot 版本 | 推荐 XXL-Job 版本 | 说明 |
|---|---|---|
| 2.x(2.6 以下) | 2.3.1 | 老版本稳定,社区资料多 |
| 2.6.x / 2.7.x | 2.4.0+ | 需要处理循环引用问题,2.4.0 已适配 |
| 3.x | 2.4.0+ | 注意检查 jakarta 命名空间等依赖兼容问题 |
3.2 application.yml 中执行器配置项逐项解析
引入依赖之后,在application.yml里增加如下配置:
xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin accessToken: default_token executor: appname: my-springboot-app address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30这些配置项我逐个解释一下,因为每个都可能在特定场景下坑到你。
xxl.job.admin.addresses:调度中心的完整地址。如果调度中心集群部署了多台,用逗号分隔多个地址即可。注意这里的地址是"调度中心对外提供接口的根路径",不是首页地址,所以要以/xxl-job-admin结尾。
xxl.job.accessToken:通信令牌,必须与调度中心的xxl.job.accessToken完全一致。这个不一致会导致调度中心调用执行器时返回 500 或 401,而且调度日志里只会看到一句很笼统的失败原因。
xxl.job.executor.appname:执行器在调度中心里的名字,也是执行器的唯一标识。这一步的坑在于,你在这里填了什么,后面在 Admin 后台"执行器管理"新增执行器时,AppName 必须一模一样,大小写和符号都不能差,否则执行器注册不上。
xxl.job.executor.ip:这一项强烈建议留空。XXL-Job 会通过InetAddress自动获取本机 IP,留空时它会扫描网卡拿到一个内网 IP。但如果你猜过这台机器,你知道多网卡环境下自动获取的 IP 不一定是调度中心能访问到的那个。如果出现注册地址不对,可以在这一项手动指定 IP。默认留空就好,让它自动判断。
xxl.job.executor.port:执行器的 HTTP 端口,默认是 9999。这个端口是执行器用来接收调度中心回调请求的监听端口,需要保证该端口没有被占用,且防火墙放行。如果你的服务部署在容器里,记得在 Dockerfile 或 Service 的端口映射里把这个端口也暴露出去。
xxl.job.executor.logpath:执行器运行日志的落盘路径。执行器在执行业务逻辑时,XxlJobHelper.log()输出的内容会先写到这个目录下的日志文件,再传给调度中心展示。这个目录必须存在且对运行用户可写,否则任务能执行但日志看不到,排查问题时会很头疼。
xxl.job.executor.logretentiondays:日志保留天数,我一般设置 30 天,过期日志会自动清理。这个属性是防止日志文件无限增长把磁盘打满的,生产环境最好设置一个合理的值。
3.3 执行器配置类与自动注册原理
配置文件写完之后,还需要创建一个 Java 配置类,把XxlJobSpringExecutor注册到 Spring 容器中。这个类是执行器组件的核心入口,它的作用简单说就是:启动时把当前应用注册到调度中心,同时维护一个"任务方法映射表",收到调度请求后根据 JobHandler 名称找到对应的方法并执行。
@Configuration public class XxlJobConfig { @Value("${xxl.job.admin.addresses}") private String adminAddresses; @Value("${xxl.job.accessToken}") private String accessToken; @Value("${xxl.job.executor.appname}") private String appname; @Value("${xxl.job.executor.port}") private int port; @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.setAccessToken(accessToken); executor.setAppname(appname); executor.setPort(port); executor.setLogPath(logPath); executor.setLogRetentionDays(logRetentionDays); return executor; } }这里想多说一句原理。XxlJobSpringExecutor实现了SmartInitializingSingleton接口,在 Spring 容器完成所有单例 Bean 的实例化之后,它会扫描容器中所有带@XxlJob注解的方法,把这些方法注册到内部的 map 结构中,映射关系就是注解里的名字和方法的实际调用关系。随后它通过 HTTP 请求向调度中心上报自己这台机器的实例信息,调度中心收到后写入xxl_job_registry表,执行器就完成了在线注册。
这个注册过程不是一次性的,执行器会按大约 30 秒一次的心跳周期持续上报,调度中心在 90 秒内没收到某个执行器的心跳,就会把它标记为离线。所以如果看到执行器时上时下,大概率是网络不通或者心跳周期不对。
3.4 在调度中心添加执行器并验证注册
执行器配置类和业务代码写完、启动 Spring Boot 应用之后,回到 Admin 后台,左侧"执行器管理"页面,点击"新增执行器":
- AppName:填
my-springboot-app,必须和application.yml里的xxl.job.executor.appname完全一致。 - 名称:随便填,比如"订单服务",仅用于后台展示。
- 注册方式:选"自动注册"。
- 机器地址:自动注册模式下不需要填,手动注册模式才需要填具体 IP:PORT。
保存之后,如果执行器已经启动,等几秒钟刷新页面,在"OnLine 机器地址"这一列就能看到类似192.168.1.20:9999的地址。看到这个地址,说明执行器已经成功注册到调度中心了。
一个常见问题是:Spring Boot 应用已经启动了,但这里的在线机器地址一直为空。排查思路是:先看服务日志里有没有报错,比如连接不上调度中心、AccessToken 不一致;如果没有报错,在服务器上 curl 一下执行器的端口确认接口是否响应:
curl http://192.168.1.20:9999/正常情况下会返回一串 JSON 或空白,但至少 TCP 能通。如果 curl 都连不上,多半是防火墙或者容器端口映射的问题。
4. 写第一个任务 Handler 并跑通调度链路
4.1 基于 @XxlJob 注解的任务实现
执行器注册成功,接下来的重头戏就是写任务了。XXL-Job 支持两种任务模式,一种是BEAN模式,直接通过@XxlJob注解标记一个 Spring Bean 的方法为任务方法;另一种是GLUE模式,允许在调度中心在线编写和修改任务代码,无需发版就能调整逻辑。日常项目里 BEAN 模式用得最多,先讲这个。
新建一个组件类,在里面定义任务方法:
@Component public class SampleXxlJob { @XxlJob("demoJobHandler") public void demoJobHandler() throws Exception { XxlJobHelper.log("XXL-JOB, Hello World."); XxlJobHelper.log("当前时间: {}", LocalDateTime.now().toString()); // 这里写你的业务逻辑 // 比如查库、调接口、批量处理数据 XxlJobHelper.handleSuccess("执行成功"); } }这里最关键的一点是:方法名无所谓,类名无所谓,但@XxlJob注解里的值——demoJobHandler——才是这个任务的全局唯一标识。后面在调度中心配置任务时,填的 JobHandler 就必须是这个名字。如果填错了,调度中心会提示"没有找到对应的 JobHandler"。
再强调一下XxlJobHelper的用法。这个工具类里的log()方法,会把日志写到执行器的日志文件里面,同时回传到调度中心,你在 Admin 后台的"执行日志"里能看到。而如果你用System.out.println或者log.info,这些日志只会在业务应用本地文件里出现,调度中心是完全看不到的。所以任务里要输出关键信息,请一律用XxlJobHelper.log()。
XxlJobHelper.handleSuccess()和handleFail()则是主动标记任务的成功或失败状态。如果任务方法正常执行完毕没抛异常,即使不调用任何方法,任务也会被标记为成功;但如果你希望任务能返回一些更明确的提示信息,或者希望在业务逻辑中主动判断某一步失败并立即结束,用这两个方法会更清晰。
4.2 调度中心配置任务:JobHandler、cron 与路由策略
任务类写好后,回到 Admin 后台,左侧"任务管理",点击"新增任务"。这里字段比较多,我把最重要的几个逐一说清楚。
执行器:下拉框里选择我们刚添加的那个执行器,比如"订单服务"。
JobHandler:填demoJobHandler,和代码里@XxlJob注解的值保持一致。
运行模式:选BEAN。如果选了GLUE(Java),调度中心会给你一段可编辑的 Java 代码,让你在线写任务逻辑,这种方式不需要发布应用,但使用场景相对特殊,后面再展开。
Cron 表达式:填0 0/1 * * * ?,表示每分钟执行一次。XXL-Job 用的是 Quartz 风格的 6 位 cron,和 Spring 的 6 位表达式的顺序是一致的,基本可以直接套用。
路由策略:这是 XXL-Job 比较核心的功能之一。默认"第一个",意思就是固定选第一台在线机器执行。如果你有多个执行器实例,可以选择"轮询""随机""故障转移""分片广播"等策略。单机调试用"第一个"就够。
阻塞处理策略:任务执行时间超过下一次调度时间点时的处理方式。单机串行表示下一次调度排队等上一次执行完;丢弃后续调度表示下一次直接跳过;覆盖之前调度表示终止上一次的下一次直接执行。默认选单机串行最安全。
失败重试次数:这里填 0 表示不重试,填 2 表示失败后自动重试 2 次。生产环境的任务我一般根据业务幂等性来决定,非幂等任务不建议开重试。
任务参数:这个字段可以填任意字符串,在任务代码中通过XxlJobHelper.getJobParam()获取。这个参数是任务级别的配置,不用改代码就能动态改变任务行为,后面讲进阶玩法时会专门演示。
配置完成后保存,然后回到任务列表,可以看到刚才创建的这条任务。如果你没改 cron 的启动状态,任务默认是开启的,到点就会自动触发。
4.3 查看调度日志与执行日志
任务创建后,建议先不要干等 cron 触发,直接在任务列表右侧点击"执行一次",手动触发一下,然后马上进"调度日志"页面看结果。
调度日志页面有非常清晰的表格:调度时间、调度结果、执行结果、执行耗时、执行机器地址。点击"执行日志"按钮,你会看到完整链路:
2025-01-12 14:30:01 [com.xxl.job.core.thread.JobThread] - 触发调度请求 2025-01-12 14:30:01 [com.xxl.job.core.thread.JobThread] - 任务执行成功展开详情,你还能看到XxlJobHelper.log()输出的内容。如果任务中加了业务日志,在这里就能直接定位问题,不用再去翻应用服务器上的日志文件了。这也是很多人一旦用了 XXL-Job 就回不去@Scheduled的重要原因——排障效率真的提升太多。
4.4 链路验证与常见状态解释
跑通一个任务后,把调度日志里的状态解释一下,方便你做判断:
- 调度成功、执行成功:链路正常,任务逻辑正常完成。
- 调度成功、执行失败:调度中心已经把请求发到执行器,但执行器跑任务时抛了异常,看执行日志定位原因。
- 调度失败:调度中心未能把请求发到执行器,可能原因包括执行器离线、AccessToken 不一致、端口不通。
另外有个细节:调度中心界面上的"执行结果"和"实际业务是否成功"是有可能不一致的。比如你任务里调用了一个外部接口,接口返回了错误码,但你的代码没有主动抛异常,任务依然会显示"成功"。所以在业务代码里,凡是关键路径成功与否,最好通过XxlJobHelper.handleFail()或主动抛异常来让状态真实反映到调度中心。
5. 进阶玩法:分片广播、动态参数与失败重试
5.1 分片广播:把海量数据拆到多台实例并行处理
任务跑通只是开始,实际业务里一定会遇到"单台执行太慢"的问题。比如一个任务要处理全量用户,数据量几百万,单实例要跑几十分钟,业务方不乐意。这时候就要用分片广播策略了。
路由策略选择"分片广播"后,调度中心会把同一个任务同时分发给所有在线执行器,并且每个执行器会拿到一个分片编号shardIndex和总分片数shardTotal。比如你有 3 台执行器在线,那么每台拿到的分片编号分别是 0、1、2,总分片数是 3。业务代码里只需要按照分片规则取模,就能让每台机器只处理自己负责的数据。
@XxlJob("shardingJobHandler") public void shardingJobHandler() { // 当前执行器的分片索引:0 ~ shardTotal-1 int shardIndex = XxlJobHelper.getShardIndex(); int shardTotal = XxlJobHelper.getShardTotal(); XxlJobHelper.log("当前分片: {} / {}", shardIndex, shardTotal); // 模拟一批用户ID List<Long> userIds = getAllUserIds(); // 每个执行器只处理 userId % shardTotal == shardIndex 的数据 for (Long userId : userIds) { if (userId % shardTotal == shardIndex) { processUser(userId); } } }判断一个用户该由哪台机器处理,最常用的方式就是userId % shardTotal。这种水平拆分的思路在极大提升处理速度的同时也带来一个问题:一旦某个执行器实例在任务执行过程中挂掉,这个实例负责的那一部分数据就没有人处理了。所以使用分片广播时,建议配合失败重试策略,或者业务上接受短时数据延迟。
5.2 任务动态参数:不改代码改变任务行为
很多任务在不同场景下需要跑不同的数据范围,比如"按日期补跑某一天的数据"、按照业务类型只处理特定类型的数据。XXL-Job 在任务配置里提供的"任务参数"字段,就是干这个用的。
任务里获取参数的方法很直接:
@XxlJob("paramJobHandler") public void paramJobHandler() { String jobParam = XxlJobHelper.getJobParam(); XxlJobHelper.log("收到任务参数: {}", jobParam); // 假设参数格式是 date=2025-01-01&type=ORDER if (StringUtils.hasText(jobParam)) { Map<String, String> paramMap = parseQueryString(jobParam); String date = paramMap.get("date"); String type = paramMap.get("type"); // 按参数处理业务 } }比如业务方临时要补跑某一天的数据,你不需要改代码,在调度中心把"任务参数"改成date=2025-01-01&type=ORDER,手动触发一次就完事。这个设计对数据修复场景非常友好,简直就是运维救星。建议在任务设计时就把参数规则约定好,比如都用key=value格式,多个参数之间用&分隔,方便解析和兼容扩展。
5.3 失败重试、超时控制与父子任务依赖
关于失败重试,我建议按任务类型区别对待。纯读取类任务,比如同步缓存、拉取接口数据,失败重试是安全的,可以设置 2-3 次。有写操作的任务,比如发短信、扣款、生成对账单,重试之前一定要确认业务本身是否幂等,否则重试可能导致重复扣款或者重复发消息。
超时控制方面,在任务管理的高级配置里有"任务超时时间"字段,单位是秒。任务执行超过这个时间,执行器会主动中断这个任务的执行线程,并在日志里标记为超时失败。对于那些可能有死循环或慢 SQL 的任务,强烈建议设置一个合理的超时时间,避免任务线程长时间占用,导致后期任务堆积。
父子任务依赖也是一个很实用的功能。比如"先同步数据,数据准备完成后再生成报表",可以在任务 A 的配置里设置"子任务 ID"为任务 B,这样 A 执行成功之后会自动触发 B。不过要注意,这个"子任务"触发的逻辑是 A 成功后才把 B 加入调度队列,如果配置了失败重试,A 重试成功后才触发 B。链路较长时,建议把每个节点的日志打清楚。
6. 实战踩坑:版本冲突、注册掉线与日志丢失
6.1 Spring Boot 版本过高导致的启动失败
这个坑在我接手的一个新项目里踩过。项目用的是 Spring Boot 2.7.16,我引入了xxl-job-core2.3.1,启动时直接报了一个诡异的错误:
The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | xxlJobSpringExecutor └─────┘原因就是 Spring Boot 2.6 之后默认禁止了 Bean 循环依赖,而老版 XXL-Job 的XxlJobSpringExecutor内部存在循环引用。网上一搜,很多帖子会告诉你,在配置里加一行spring.main.allow-circular-references=true把循环依赖放行。这个方案确实能启动,但我不推荐一上来就这么干。
更好的做法是升级 XXL-Job 到 2.4.0 或更新版本,官方在新版本里重构了这块逻辑,不需要放开循环依赖限制就能正常工作。如果你因为兼容性原因只能留在老版本,再考虑那个兜底开关。另外提醒一句:如果项目本身用了 Spring Boot 3.x,务必确认你引入的xxl-job-core版本支持 Spring Boot 3,避免出现命名空间或依赖冲突的问题。
6.2 执行器注册不上或注册后立即掉线
执行器注册不上,不要慌,按这条路径排查。
第一步看服务启动日志。如果配置了debug级别日志,XXL-Job 启动时通常会打印注册相关的调用信息,比如xxl-job register executor success或fail。如果完全没有相关日志,先确认XxlJobSpringExecutor这个 Bean 是否真的被 Spring 容器扫描到了,配置类的位置和启动类的关系有时候会让人忽略。
第二步看 IP 和端口。多网卡机器上最常见的坑就是注册了错误的 IP,比如服务器上有eth0和docker0两块网卡,自动获取到的 IP 是一个 172.17.x.x 的 Docker 网段地址,调度中心在另一个网段,根本访问不到。这种情况就是在application.yml里手动指定xxl.job.executor.ip为业务网卡对应的 IP。
第三步看在线状态。如果注册上之后几秒又掉线,基本是心跳不通。检查执行器的 9999 端口是不是只监听了本机或者被防火墙拦截了:
netstat -tlnp | grep 9999如果端口是好的,再用curl从调度中心所在机器访问执行器端口验证网络连通性。容器化部署时还要特别注意,9999端口必须映射到宿主机外部,否则调度中心永远访问不到执行器。
6.3 任务执行成功但调度中心却看不到日志
这个问题的表现形式是:任务在业务系统里确实执行了,但调度中心的任务日志要么一直显示"调度成功,执行结果为空",要么点开执行日志只有接口调用记录,没有XxlJobHelper.log()输出的内容。
原因很可能是xxl.job.executor.logpath配置的目录不存在或没有写权限。执行器在写日志的时候,如果目录不可写,会静默失败,任务该跑还是跑,但日志回传就是空的。解决办法是提前把目录创建好并赋予写权限:
mkdir -p /data/applogs/xxl-job/jobhandler chown -R 应用运行用户:应用运行用户 /data/applogs/xxl-jobWindows 本地调试时,把logpath配置成一个真实存在的目录,比如D:/logs/xxl-job/jobhandler,避免测试环境莫名其妙看不到日志。
6.4 AccessToken 不一致导致调度失败
调度中心配置了accessToken,执行器也配置了accessToken,但发现任务始终调度失败。检查一下是不是有多个环境、多套配置,导致某个环境里的执行器用的accessToken和调度中心不一致。这种问题在本地调试时尤其隐蔽,因为本地可能有一套配置,测试环境又有另一套。
排查方式是看调度中心的调度日志,失败原因如果类似"xxl-job access token is invalid",那就明确是 token 不匹配。把两边的xxl.job.accessToken改成同一个值,重启执行器即可。如果你不想用 token 校验,两边都留空也是可以的,但生产环境强烈建议加上,防止内网里有人伪造请求触发你的任务。
6.5 集成测试时如何绕开调度中心
最后分享一个测试相关的经验。Spring Boot 项目的单元测试或集成测试,如果直接启动整个 Spring 容器,XxlJobSpringExecutor会尝试连接调度中心。测试环境如果连不上调度中心,这些用例就会失败或者多出一堆无意义的超时等待。
我的做法是给配置类加一个开关,专门用于测试环境:
# application-test.yml xxl: job: enabled: false然后在配置类上加上条件注解:
@Configuration @ConditionalOnProperty(name = "xxl.job.enabled", havingValue = "true", matchIfMissing = true) public class XxlJobConfig { // 配置内容不变 }这样测试环境的配置里把xxl.job.enabled设为false,执行器就不会启动,也不会尝试注册到调度中心。测试用例可以专注于业务逻辑本身,不会被外部依赖干扰。
我这两年用下来,XXL-Job 在绝大多数场景下都能解决分布式定时任务的管理问题。刚开始接入时可能会被它那一堆配置项和策略选项唬住,但其实核心链路就一条:调度中心负责调度,执行器负责干活,JobHandler 把两边串起来。把这条主线跑通,再去根据业务场景调整路由策略、失败重试和分片规则,整个体系的掌控感就完全不一样了。