1. 先说清楚为什么Spring Boot项目要引入XXL-JOB前后端项目做多了你会发现业务里真正难搞的往往不是接口怎么写而是那些“到点必须跑、跑了不能挂、挂了要重试、多台机器只能跑一份”的定时任务。单机项目用Spring自带的Scheduled当然没问题但一旦服务做了多实例部署Scheduled就开始暴露问题了每个实例都会执行一遍任务数据重复处理、接口重复调用、库存重复扣减灾难现场。XXL-JOB解决的就是这个痛点。它是一个轻量级的分布式任务调度平台核心设计是“调度中心”和“执行器”分离调度中心负责统一管理任务、触发任务、查看日志执行器负责真正干活。你在调度中心配置一条任务指定路由策略比如轮询、故障转移、分片广播调度中心就会按照策略把任务派发给对应的执行器实例。整个过程对业务代码侵入性很低Spring Boot项目接入成本也就二十分钟左右。这篇博文我从零开始捋一遍Spring Boot整合XXL-JOB的完整过程包括调度中心怎么搭、执行器怎么配、任务怎么写、参数怎么传、日志怎么查以及我在生产环境里踩过的一些坑。已经在用Scheduled的兄弟也能对照着看看什么时候该切换到XXL-JOB切换的时候有哪些注意点。2. 整合前的准备工作与核心概念2.1 版本选型和环境依赖XXL-JOB的版本迭代节奏不算快但不同版本之间的配置项和行为差异还是有的。我这边用过2.3.1和2.4.0两个版本整体来说2.3.1最稳网上资料也最多遇到问题基本都能搜到解决方案2.4.0开始UI和底层调度逻辑有一些调整如果你是新项目直接用2.4.0没问题。三个关键依赖JDK 8及以上我用的是JDK 8生产环境很稳Maven 3.6MySQL 5.7调度中心需要数据库存储任务和日志如果你项目里用的是Spring Boot 2.x那和XXL-JOB 2.3.1/2.4.0配合都没有问题。Spring Boot 3.x用户需要注意一下XXL-JOB官方对Spring Boot 3的适配包是单独的不要拿旧的1.x/2.x版本硬接会出现一堆兼容性问题。2.2 调度中心和执行器的职责划分刚接触XXL-JOB的同学容易把这两个角色搞混。调度中心xxl-job-admin本身是不执行任务的它只负责任务配置管理、触发调度、调度日志记录、执行器注册管理。真正干活的是执行器executor它嵌入在你的Spring Boot业务应用里启动时自动注册到调度中心然后等待调度中心的任务指令。打个比方调度中心是“大脑”执行器是“手脚”。大脑决定几点几分做什么事、派哪个手去做、做完记录一下手脚只负责执行执行完把结果回报给大脑。这样拆开之后你的业务服务可以随便扩容缩容只要执行器能注册上来调度中心就永远知道现在有哪些实例可以干活。2.3 调度中心初始化数据库表和默认账号XXL-JOB调度中心需要先建库建表。官方在源码目录里提供了tables_xxl_job.sql脚本直接在MySQL里执行即可。执行完后会生成xxl_job相关的系列表核心的几张表作用如下表名作用xxl_job_info任务配置信息包括cron表达式、路由策略、JobHandler名称等xxl_job_log调度日志记录了每次任务的触发时间、执行结果、失败原因xxl_job_registry执行器注册表执行器在线离线状态都在这xxl_job_group执行器分组一个执行器对应一个AppNamexxl_job_user调度中心登录用户表初始化完成后启动xxl-job-admin应用默认端口8080浏览器访问http://localhost:8080/xxl-job-admin默认账号admin/admin123可以登录。进去之后你会看到一个管理后台左侧菜单有执行器管理、任务管理、调度日志等功能。第一次登录建议先去执行器管理页面看看目前应该是空的等你的Spring Boot项目启动注册后这里就会出现一条执行器记录。3. Spring Boot项目接入执行器的详细步骤3.1 引入Maven依赖执行器的接入非常简单本质上就是引入一个Maven依赖。我用的是2.4.0版本依赖如下dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.0/version /dependency如果你用的Spring Boot版本比较老比如1.x那注意排除掉xxl-job-core自带的Spring Boot相关依赖避免版本冲突。但既然都是新建项目直接用Spring Boot 2.x就好不需要纠结这一点。3.2 配置文件执行器核心参数在application.yml里加入XXL-JOB相关配置。需要注意这里的配置项是XXL-JOB自己的配置体系不走Spring Boot的ConfigurationProperties自动绑定它是通过XxlJobConfig这个配置类手动读取并创建Bean的。xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin accessToken: default_token executor: appname: xxl-job-executor-demo address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30逐项解释一下这些都是有讲究的admin.addresses调度中心地址多个地址用逗号分隔。如果是集群部署的调度中心这里就填所有调度中心地址。accessToken调度中心和执行器之间的通信令牌两边要一致否则执行器注册不上。生产环境必须改掉默认值这相当于调度中心和你服务之间的密钥。executor.appname执行器名称这个值要和调度中心“执行器管理”里配置的AppName保持一致是执行器分组的唯一标识。executor.port执行器HTTP服务端口用来接收调度中心的调度请求。这个端口号在同一个机器上跑多个执行器实例时不能重复。executor.logpath任务日志输出路径。排障的时候要到这里看详细日志别只盯着调度中心的日志看。executor.logretentiondays调度中心日志保留天数这里配的是30天到期后会自动清理避免日志文件把磁盘占满。3.3 配置类注入XxlJobExecutor Bean有配置文件还不够需要写一个配置类把XxlJobExecutor交给Spring容器管理。官方示例的写法是这样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.address}) private String address; Value(${xxl.job.executor.ip}) private String ip; 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 xxlJobSpringExecutor new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; } }这里有个细节我要多说一句XxlJobSpringExecutor这个类的作用不只是注册执行器它还负责扫描Spring容器里所有带XxlJob注解的方法。所以如果你发现任务能注册但是调度的时候找不到Handler大概率就是配置类没有生效或者执行器没起来。3.4 启动项目验证注册结果配置类写完直接启动Spring Boot应用。正常情况下控制台会输出类似这样的日志xxl-job register executor success, appnamexxl-job-executor-demo, addresshttp://192.168.1.100:9999/这时候去调度中心的“执行器管理”页面刷新可以看到xxl-job-executor-demo出现在在线列表中。如果执行器地址是内网IP可能会显示类似http://192.168.x.x:9999/的地址点“查看”还能看到注册的IP和端口信息。我遇到过一种情况项目能正常启动但是执行器就是注册不上去调度中心一直显示离线。后来排查发现是accessToken两边配置不一致。调度中心启动参数里配置的accessToken是default_token我业务这边改成了自定义字符串两边对不上注册请求直接被拒。这个问题不报错、不警告就在日志里轻描淡写一行非常容易忽略。4. 任务开发从最简单的JobHandler开始4.1 注解方式和Bean模式的取舍XXL-JOB的任务开发有两种方式一种是Bean模式在方法上加XxlJob注解方法名就是JobHandler名称另一种是Glue模式任务代码直接写在调度中心上不用重启业务应用。中小型项目建议直接上Bean模式代码和业务在一起方便走Git管理、代码评审、版本回溯。Glue模式主要用在那种“改个任务逻辑还得重启整个应用”的尴尬场景但它在生产上调试不太方便我一般不太推荐核心链路的任务用Glue。4.2 一个完整可跑的示例任务Component public class DemoJobHandler { XxlJob(demoJobHandler) public void demoJobHandler() throws Exception { XxlJobHelper.log(demo task start ...); // 模拟业务处理 Thread.sleep(2000); XxlJobHelper.log(demo task end ...); XxlJobHelper.handleSuccess(任务执行成功); } }任务代码写完后回到调度中心“任务管理”页面新建一个任务执行器选择xxl-job-executor-demo任务描述随便填比如“示例任务”调度类型CRON填写0 0/1 * * * ?每分钟执行一次Run模式BeanJobHandlerdemoJobHandler路由策略第一个后面会详细说阻塞处理策略单机串行推荐任务超时时间0不限制失败重试次数0配置完保存然后点击“执行一次”跑到调度日志里看结果。正常情况下日志会显示“调度成功”和“执行成功”点进去还能看到你在代码里通过XxlJobHelper.log打印的自定义日志。4.3 任务参数传递的两种方式XXL-JOB支持在调度中心配置任务参数然后在代码里读取。有两种传参方式一种是在调度中心创建任务时填写“任务参数”这个参数会以字符串形式传给任务方法另一种是调度时动态传入参数比如手动触发执行时填的参数。代码里通过XxlJobHelper.getJobParam()读取XxlJob(paramJobHandler) public void paramJobHandler() throws Exception { String jobParam XxlJobHelper.getJobParam(); XxlJobHelper.log(接收到的参数: {}, jobParam); // 解析参数比如 JSON 字符串 JSONObject paramObj JSONObject.parseObject(jobParam); String bizDate paramObj.getString(bizDate); XxlJobHelper.log(业务日期: {}, bizDate); }实际开发中我建议任务参数统一约定为JSON字符串这样扩展性最好。比如一个数据同步任务可能需要传源表名、目标表名、批次大小这些都可以放进JSON里加字段不改变接口约定方便后续扩展。4.4 任务里写日志的正确姿势这一点值得专门强调。XXL-JOB里可以用XxlJobHelper.log()来打印日志这些日志会收录到调度中心的后台日志界面你在网页上就能看到。千万别用System.out.println或者logback直接输出那样排障的时候你只能去服务器上翻日志文件效率低好多。我在实际生产里吃过这个亏。当时一个任务偶发失败我在代码里用的是log.info(batch {} process success, batchNo)结果任务失败后调度中心日志里只能看到“执行失败”四个字根本没有上下文。后来改成XxlJobHelper.log(batch {} process success, batchNo)问题定位快了很多。所以从接入第一天开始就养成用XxlJobHelper打日志的习惯。5. 调度中心的关键配置与生产建议5.1 路由策略怎么选执行器路由策略这块我直接给结论性建议路由策略适用场景注意事项第一个测试环境、单机部署固定打到第一台机器轮询多实例负载均衡执行适合无状态任务故障转移关键任务、高可用诉求一台失败自动切换下一台分片广播大数据批处理场景每台机器处理一部分数据一致性HASH需要固定IP执行的任务同参数任务固定打到同一台机器分片广播值得多说一句。如果任务是全量数据清洗比如从订单表里扫数据做统计10台机器一起干肯定比1台机器快10倍。XXL-JOB的分片广播会把任务同时分发到所有执行器每个执行器拿到不同的shardIndex和shardTotal。你的代码里拿到这两参数后自己决定处理哪些数据。XxlJob(shardingJobHandler) public void shardingJobHandler() throws Exception { int shardIndex XxlJobHelper.getShardIndex(); int shardTotal XxlJobHelper.getShardTotal(); XxlJobHelper.log(分片信息: index{}, total{}, shardIndex, shardTotal); // 假设订单表 id 从 1 到 10000 int start shardIndex * (10000 / shardTotal); int end (shardIndex 1) * (10000 / shardTotal) - 1; XxlJobHelper.log(本分片处理范围: {} - {}, start, end); }如果你没用过分片第一次跑的时候建议先在测试环境打印一下分片信息确认各个执行器拿到的分片范围没有重叠、没有遗漏再上生产。5.2 阻塞处理策略任务阻塞处理策略解决的是“上一次任务还没跑完下一次调度又来了”的问题。常见三种单机串行排队执行上一次跑完才执行下一次丢弃后续调度上一次没跑完这次触发直接丢弃覆盖之前调度直接终止上次任务重新开始这次我建议默认用“单机串行”这是最安全的策略。如果任务本身就是追求速度的比如定时拉取数据用“丢弃后续调度”也可以接受。至于“覆盖之前调度”除非你有非常明确的需求比如必须保证最新一次任务尽快执行否则别用终止任务会引发一些资源未释放的问题。5.3 调度中心的集群部署调度中心单点也是一个隐患。如果调度中心挂了所有任务都不会被触发业务直接停摆。生产环境建议至少部署两个调度中心实例共用同一个数据库。这样一台挂了另一台还能继续调度。集群部署时调度中心的配置文件里不需要改什么东西多个实例之间没有状态同步的问题因为状态都在数据库里。唯一要注意的是多台调度中心实例之间的时间尽量同步建议在服务器上配好NTP时间同步否则调度的秒级别会乱掉。5.4 任务超时和失败重试的合理配置任务超时时间和失败重试次数是生产事故的隐形源头。如果你给任务配了超时时间10秒而这个任务正常情况就要跑30秒那结果就是任务一直被中断重来业务数据永远处理不完。失败重试也是一样重试次数过多遇到的是外部接口超时问题重试10次就是10倍压力放大。建议遵循这几个原则不确定任务耗时的超时先填0不限失败重试次数生产环境先配1-2次排查稳定后再决定是否增加重试要配合“调度过期策略”使用防止迟到任务堆积5.5 调度过期策略的作用任务调度的时候如果执行器所在机器繁忙或者网络抖动可能出现任务没有在规定时间执行的情况。XXL-JOB定义了“调度过期策略”任务错过了调度时间后是否补偿执行。这适合处理任务堆积的场景。我的建议是关键任务选“忽略”非关键任务选“立即执行一次”。举个实际场景凌晨的数据统计任务如果1点钟那次调度因为数据库锁被错过了2点钟那次的调度又来了如果选择立即执行可能会把1点和2点的数据一起处理掉但实际上是重复处理。这种场景更适合忽略错过的调度等下一周期正常执行。6. 常见问题与排查技巧实录6.1 执行器一直显示离线按顺序排查检查业务项目日志里有没有“register executor success”字样没有的话说明执行器根本没启动成功检查accessToken是否一致调度中心启动参数和业务配置要完全相同检查执行器端口是否被防火墙拦截executor.port如果不填会随机选端口生产环境建议固定检查调度中心地址是否可达curl一下试试6.2 调度成功但执行失败这种情况大概率是JobHandler名称对不上。调度中心配置的JobHandler必须和代码里XxlJob注解的值完全一致注意大小写和驼峰命名。报错信息一般是“job handler [xxx] not found”。另外一个常见原因是执行器里没有扫描到任务类Component注解漏了或者扫描包路径不对。检查一下类上有没有被Spring容器管理。6.3 任务重复执行出现重复执行先看是不是部署了多个执行器副本路由策略又是“第一个”以外的策略。比如轮询策略下两个实例都会收到任务。再加上你的业务里没有做幂等控制数据就会重复处理。解决办法要么把路由策略调整为“第一个”并固定到一个实例上要么在业务侧增加幂等机制比如处理前先查一下数据状态位。在这里我给所有人一个建议无论你怎么配路由策略任务一定要做幂等。分布式环境下网络抖动、重试机制、时钟误差都可能导致任务重复触发幂等是最保险的兜底方案。6.4 调度日志查不到如果任务执行了但是调度中心日志为空先看下执行器日志文件。executor.logpath配置的路径下会有近期日志翻一下是否有异常堆栈。同时确认一下服务器时间是否准确XXL-JOB的日志是按时间滚动的如果业务服务器时间和调度中心时间差太大日志可能对不上。6.5 数据库连接池满了这是高并发场景下比较隐性的问题。执行器处理任务时占用的数据库连接没有及时释放任务一多连接池就满了。这种情况优先排查任务方法的事务边界检查是不是把耗时操作包在大事务里了。另外可以调大连接池上限但这只是缓解不是根治。7. 生产环境接入XXL-JOB的清单式总结最后给出一份我每次接入XXL-JOB时的核对清单按顺序执行基本不会漏准备好MySQL并执行官方SQL脚本单独部署xxl-job-admin调度中心修改accessToken业务工程引入xxl-job-core依赖并配置执行器参数写配置类注入XxlJobSpringExecutor启动业务应用确认调度中心执行器在线开发JobHandler使用XxlJobHelper打印日志调度中心配置任务先用CRON测试频率跑通确认路由策略、阻塞处理策略、超时时间、重试次数测试参数传递和分片逻辑生产放量前确认幂等方案XXL-JOB这工具本身不复杂真正决定它好不好用的是你在业务里怎么设计任务、怎么控制并发、怎么做好日志和监控。很多团队用了一阵子之后就把它当成一个简单的定时任务平台忽略了调度中心本身的监控告警能力。其实你可以在调度中心对任务执行失败率、执行耗时做统计配合邮件告警把异常情况第一时间暴露出来。我现在的团队还接入了统一监控任务失败和耗时异常都会触发告警建议你在生产环境也尽早把这一环补上。还有一个小技巧XXL-JOB的调度日志支持按任务ID、触发时间、执行结果筛选遇到批量任务异常时先按“失败”筛选再按时间倒序快速锁定问题发生点。这个操作看起来不起眼但真正到凌晨三点被拉起来处理生产故障的时候就能帮你省下十几分钟的排查时间。这个项目本身算是比较标准的中间件接入流程麻烦的是注册不上执行器、Handler找不到这类环境问题。好在这些问题的排查路径相对固定按上面清单走一遍基本都能解决。如果是在微服务架构里做接入建议把执行器配置做成公共配置沉淀到基础包里这样每个业务服务接入的成本会再低一些。