
1. 从一个典型的定时任务痛点说起如果你负责过几个后台服务大概率遇到过这样的场景业务里总有一些需要定时执行的任务比如每天凌晨清理临时文件、每小时同步一次用户数据、每五分钟检查一次订单状态。最开始你可能会图省事直接用Scheduled注解在 Spring Boot 应用里写个方法或者用 Linux 的 crontab 写个脚本。项目初期这确实简单高效。但随着业务发展问题就接踵而至了。服务需要部署多个实例做集群结果每个实例上的定时任务都同时执行造成了重复处理。你想关掉某个实例的任务却发现它和业务代码耦合在一起重启应用才能生效。某个任务执行失败了你只能去翻几万行的日志文件手动排查原因。更头疼的是任务执行时间、频率需要调整时你得改代码、重新打包、部署上线运维同学看你的眼神都带着杀气。这时候一个集中式的、可视化的、支持分布式协调的定时任务调度平台就成了刚需。而XXL-JOB正是为了解决这些问题而生的一个轻量级分布式任务调度框架。它把任务的调度逻辑什么时候触发、触发哪个任务和任务的执行逻辑具体做什么事分离开来形成了“调度中心”和“执行器”两个核心角色。调度中心负责任务的集中管理和触发执行器则是一组承载具体业务逻辑的服务器。这种架构让任务管理变得像在网页上操作一样简单直观。今天我就结合自己多次在项目中落地 XXL-JOB 的经验抛开官方文档的条条框框带你从零开始把调度中心搭起来再把你的 Spring Boot 应用变成一个听话的执行器最后跑通一个最简单的任务。我们会重点关注那些文档里一笔带过但实际部署时一定会踩到的坑。2. 调度中心部署选对版本和数据库是关键第一步部署调度中心听起来就是下载、改配置、启动。但第一步选型不对后面可能全是坑。XXL-JOB 的调度中心是一个独立的 Java Web 应用你需要先把它跑起来。2.1 版本选择与源码获取首先我强烈建议直接从 GitHub 的官方仓库获取源码和发行版 https://github.com/xuxueli/xxl-job 。为什么要用源码因为很多生产环境的定制化需求比如数据库适配、登录逻辑改造都需要修改源码直接使用 JAR 包会限制你的手脚。截至我写这篇文章时2.4.0 是一个经过大量生产验证的稳定版本。3.x 版本虽然功能更丰富但架构有较大调整如果你是首次引入从 2.x 开始会更稳妥。下载后你会得到一个标准的 Maven 多模块项目。我们关注的核心是xxl-job-admin模块这就是调度中心。2.2 数据库初始化与配置的“魔鬼细节”调度中心的所有任务元数据、日志、执行记录都需要存到数据库。官方默认支持 MySQL。执行项目根目录/doc/db/tables_xxl_job.sql这个脚本初始化数据库表。接下来是重头戏配置xxl-job-admin模块下的application.properties。这里有几个极易出错的点# 数据库连接注意时区和服务端编码 spring.datasource.urljdbc:mysql://your-mysql-host:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8autoReconnecttrueserverTimezoneAsia/Shanghai spring.datasource.usernameyour_username spring.datasource.passwordyour_password spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver # 调度中心通讯TOKEN用于和执行器认证。生产环境一定要改不要用默认的。 xxl.job.accessTokenyour_production_token_here # 调度中心对外暴露的地址执行器靠这个地址来回调注册和获取任务。这是最关键的配置 xxl.job.admin.addresseshttp://your-admin-host:8080/xxl-job-admin关于xxl.job.admin.addresses我踩过一个大坑。如果你在 Docker 或 Kubernetes 中部署调度中心并且通过 Ingress 或 NodePort 对外提供服务那么这个地址必须填写外部可访问的地址。比如你的调度中心在 K8s 集群内 Service 的地址是http://xxl-job-admin:8080但执行器在集群外那么这里就必须填 Ingress 的域名例如http://xxl-job.yourcompany.com。否则执行器注册成功后调度中心下发的任务触发指令执行器会无法回调通知调度中心任务执行结果。2.3 启动与初步访问配置完成后打包xxl-job-admin模块你会得到一个xxl-job-admin-2.4.0.jar。用java -jar命令启动即可。默认端口是 8080。启动后访问http://your-admin-host:8080/xxl-job-admin。默认登录账号密码是admin/123456。登录后第一件事就是去修改密码进入管理界面你会看到清晰的仪表盘、任务管理、执行器管理、调度日志等菜单。到这里调度中心的大脑就准备就绪了。3. 执行器集成让你的Spring Boot应用“听令行事”调度中心是发号施令的指挥官执行器就是干活的士兵。我们需要将我们的业务应用通常是一个 Spring Boot 服务改造成一个执行器。3.1 依赖引入与基础配置在你的 Spring Boot 项目的pom.xml中引入 XXL-JOB 执行器客户端依赖。注意版本号与调度中心保持一致。dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.0/version /dependency接下来在application.yml(或application.properties) 中配置执行器的核心参数# XXL-JOB 执行器配置 xxl: job: admin: # 调度中心地址多个用逗号分隔。必须和调度中心配置的 xxl.job.admin.addresses 一致 addresses: http://your-admin-host:8080/xxl-job-admin # 执行器与调度中心通信的令牌需与调度中心配置的 xxl.job.accessToken 一致 accessToken: your_production_token_here executor: # 执行器AppName这是调度中心识别不同执行器集群的唯一标识非常重要 appname: your-application-name # 执行器注册方式有“自动注册”和“手动录入”两种我们通常用自动注册 address: # 自动注册时此处留空 # 执行器IP自动注册时优先使用该IP。不填则自动获取。在容器等复杂网络环境下这个配置是救命稻草。 ip: # 执行器端口默认9999。执行器内嵌一个Netty HTTP服务用于接收调度中心的触发请求。 port: 9999 # 执行器日志路径用于存储任务调度日志 logpath: /data/applogs/xxl-job/jobhandler # 执行器日志保留天数 logretentiondays: 30这里有几个关键解释appname你可以理解为项目组或服务名。比如“用户中心服务”、“订单处理服务”。一个appname下可以注册多个执行器实例即多个部署了相同代码的服务节点形成一个集群。调度中心的任务可以指定在这个appname集群中的任一机器或所有机器上执行。address与自动注册当address为空时执行器启动后会主动向admin.addresses指定的调度中心注册自己基于ip:port。这是最常用的方式。如果网络策略不允许执行器主动向外连接才考虑使用“手动录入”即在调度中心管理页面手动填写执行器的地址。ip配置的玄机这是自动注册时决定上报IP的核心。如果不配置执行器客户端会调用InetAddress.getLocalHost().getHostAddress()来获取本机IP。在简单的物理机或虚拟机环境中这通常没问题。但在 Docker 容器、K8s Pod 或复杂的多网卡环境中自动获取的IP很可能是容器内部IP如 172.17.0.2或错误的网卡IP导致调度中心无法通过网络访问到该执行器实例。因此在生产环境的容器中我强烈建议通过环境变量或配置文件显式指定xxl.job.executor.ip为宿主机的IP或该Pod能被调度中心访问到的IP。3.2 配置类与执行器Bean声明光有配置还不够需要在 Spring 的上下文中声明执行器 Bean。创建一个配置类XxlJobConfigimport com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class XxlJobConfig { private Logger logger LoggerFactory.getLogger(XxlJobConfig.class); 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:9999}) private int port; Value(${xxl.job.executor.logpath}) private String logPath; Value(${xxl.job.executor.logretentiondays:30}) private int logRetentionDays; Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info( xxl-job config init.); 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这个核心 Bean。执行器启动时这个 Bean 会完成向调度中心的注册。3.3 编写你的第一个任务处理器JobHandler现在我们来创建一个真正的定时任务。在 Spring Bean 的方法上添加XxlJob注解即可。import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; Component public class SampleXxlJob { private static Logger logger LoggerFactory.getLogger(SampleXxlJob.class); /** * 一个简单的示例任务 * 1、在调度中心新建任务时JobHandler 属性就填写这个方法名 “demoJobHandler”。 * 2、任务参数可以通过 XxlJobHelper.getJobParam() 获取。 */ XxlJob(demoJobHandler) public void demoJobHandler() throws Exception { // 通过 XxlJobHelper 获取任务上下文信息 String jobParam XxlJobHelper.getJobParam(); // 获取调度中心配置的任务参数 int shardIndex XxlJobHelper.getShardIndex(); // 当前分片序号 int shardTotal XxlJobHelper.getShardTotal(); // 总分片数 XxlJobHelper.log(XXL-JOB, Hello World! Param: {}, Shard: {}/{}, jobParam, shardIndex, shardTotal); logger.info(执行 demoJobHandler 参数: {}, jobParam); // 模拟业务处理 for (int i 0; i 5; i) { XxlJobHelper.log(beat at: i); TimeUnit.SECONDS.sleep(1); } // 默认返回成功无需显式调用 // 如果任务失败可以调用 XxlJobHelper.handleFail(失败原因); } }关键点说明XxlJob(demoJobHandler)中的value就是任务处理器的名称必须唯一。调度中心创建任务时就是通过这个名称来绑定具体的执行代码。XxlJobHelper是一个工具类提供了在任务执行过程中与调度中心交互的能力如获取参数、写日志、设置执行结果等。任务方法执行成功正常结束调度中心会收到成功状态。如果抛出异常调度中心会记录为失败。你也可以通过XxlJobHelper.handleFail()手动标记失败。XxlJobHelper.log()写入的日志可以在调度中心的“调度日志”页面实时查看这对于远程调试和监控至关重要。4. 在调度中心“牵线搭桥”配置并触发你的任务现在执行器已经准备就绪并在调度中心注册了自己在“执行器管理”页面可以看到一个名为your-application-name的执行器其下有一个地址为ip:port的机器实例。接下来我们需要在调度中心创建一个任务让它去指挥执行器干活。4.1 新建任务理解每一个配置项进入调度中心Web界面点击“任务管理”-“新增”。执行器选择我们刚才注册的your-application-name。这意味着这个任务将被下发到这个执行器集群。任务描述给自己看的写清楚这个任务干嘛的。路由策略当执行器有多个实例时任务如何分配。常用选项FIRST第一个选择集群中第一个注册的机器。ROUND轮询依次选择集群中的机器。RANDOM随机随机选择。CONSISTENT_HASH一致性哈希根据任务ID哈希保证相同ID的任务总落到同一台机器。适用于需要分片或数据本地性的场景。最不经常使用LFU、最近最久未使用LRU基于负载的调度。分片广播BROADCAST集群中所有机器同时执行一次。这个策略非常有用比如用于清理所有机器上的本地缓存。Cron定时表达式如0 0 2 * * ?表示每天凌晨2点执行。这是核心调度规则。运行模式选择 “BEAN”这是我们最常用的模式对应执行器里用XxlJob注解的方法。JobHandler这里必须填写XxlJob注解里定义的名字即demoJobHandler。大小写敏感。任务参数可以传递给任务处理器的字符串。在我们的代码里通过XxlJobHelper.getJobParam()获取。阻塞处理策略当同一个任务在上一次还没执行完时下一次调度触发来了怎么办单机串行默认排队等上一次执行完再执行下一次。丢弃后续调度忽略本次触发记一次失败。覆盖之前调度强制终止正在运行的任务执行新的。慎用任务超时时间任务执行超过这个时间会被强制标记为失败。失败重试次数任务执行失败后自动重试的次数不包括第一次失败。4.2 启动任务与实时监控保存任务后在任务列表的操作栏点击“启动”。任务就会根据 Cron 表达式开始调度了。你可以点击操作栏的“执行一次”来手动触发测试。更重要的功能是“调度日志”点击后可以看到这个任务每一次被调度的记录包括触发时间、执行器地址、执行结果、耗时以及我们在代码中用XxlJobHelper.log()打印的日志。这里是排查任务是否真正执行、执行成功与否、执行逻辑是否有问题的第一现场。5. 生产环境集群部署与网络隔离实践单机演示跑通了但生产环境往往是高可用的集群部署并且网络环境复杂。这里分享几个关键实践。5.1 调度中心的高可用部署调度中心本身是无状态的状态都存在数据库里因此实现高可用非常简单部署多个调度中心实例共用同一个数据库。你需要做的就是为调度中心申请一个虚拟IPVIP或配置一个负载均衡器如 Nginx。将xxl.job.admin.addresses配置为这个VIP或负载均衡器的地址例如http://vip-xxl-job.yourcompany.com/xxl-job-admin。所有执行器都配置这个统一地址。部署两个或更多的xxl-job-admin实例它们都连接同一个 MySQL 数据库。将负载均衡器的流量分发到这些实例上。这样即使某个调度中心实例宕机其他实例可以立刻接管执行器通过统一的地址访问感知不到后端的变化。调度中心内置了分布式锁基于数据库可以保证多个实例不会重复触发同一个任务。5.2 执行器在容器化环境中的网络配置这是问题最多的环节。假设你的执行器部署在 Kubernetes 中。问题1执行器注册的IP不对。如前所述在 K8s Pod 中如果不指定xxl.job.executor.ip客户端很可能取到的是 Pod IP如10.244.1.5而调度中心在集群外无法直接访问这个 IP。解决方案方案A推荐显式指定在 Deployment 的 Pod 模板中通过环境变量注入宿主机的 Node IP 或一个能被调度中心访问到的 Service IP。这要求你的网络策略允许调度中心访问到这个IP。env: - name: XXL_JOB_EXECUTOR_IP valueFrom: fieldRef: fieldPath: status.hostIP # 使用节点IP然后在应用配置中引用xxl.job.executor.ip: ${XXL_JOB_EXECUTOR_IP:}。这种方式最清晰但要求节点IP对调度中心可达。方案B使用K8s Service为执行器 Pod 创建一个ClusterIP或NodePort类型的 Service。将xxl.job.executor.ip配置为这个 Service 的域名如your-app-service.namespace.svc.cluster.local或NodePort对外的IP。同时xxl.job.executor.port要配置为 Service 暴露的端口。这需要调度中心能够访问 K8s 集群内部网络。问题2调度中心无法回调执行器。即使注册IP对了调度中心触发任务时是向执行器的ip:port发起一个 HTTP POST 请求。如果网络存在防火墙或安全组限制这个请求可能被阻断。解决方案确保调度中心所在服务器或网络能够访问到执行器配置的ip:port组合。这通常需要运维同事在防火墙规则或安全组中放行。在云环境下要特别注意安全组的入站规则。5.3 任务分片广播应对大数据量处理的利器这是一个高级但极其有用的功能。假设你有一个任务需要处理数据库里的 10000 条待处理数据。如果只有一个执行器实例它需要串行处理10000条很慢。如果你有5个执行器实例你希望每个实例处理2000条。这时就可以使用“分片广播”“分片参数”。在调度中心该任务的路由策略选择“分片广播”。在执行器的任务代码中通过XxlJobHelper.getShardIndex()和XxlJobHelper.getShardTotal()获取当前实例的分片序号和总分片数。在任务逻辑中根据分片信息去数据库查询属于自己那部分数据。例如XxlJob(shardingJobHandler) public void shardingJobHandler() { int shardIndex XxlJobHelper.getShardIndex(); int shardTotal XxlJobHelper.getShardTotal(); // 假设根据ID取模分片 ListData dataList dataService.findPendingDataByShard(shardIndex, shardTotal); for (Data data : dataList) { // 处理 data... } }当任务触发时调度中心会向your-application-name这个执行器集群下的每一个在线实例都发送一次任务触发请求。每个实例执行相同的代码但通过不同的shardIndex来处理不同的数据子集从而实现并行处理极大提升效率。处理大数据量的定时统计、数据迁移等场景这个功能是标配。6. 日志、监控与日常运维心得任务跑起来不是终点如何看得清、管得住才是关键。6.1 调度日志与执行器日志XXL-JOB 有两类日志调度日志存储在调度中心的数据库xxl_job_log表中。记录了每次任务调度的元信息触发时间、执行器、结果、耗时。这个日志有保留天数配置调度中心管理界面可以设置定期清理主要用于任务执行状态的宏观监控和回溯。执行器日志存储在xxl.job.executor.logpath配置的目录下。这里存放的是任务执行过程中通过XxlJobHelper.log()打印的业务日志。务必确保该目录有写入权限且磁盘空间充足。这些日志是排查任务内部逻辑问题的关键。我习惯将重要的业务执行结果、异常堆栈信息都通过XxlJobHelper.log()输出这样在调度中心页面就能直接看到无需登录服务器查看应用日志。6.2 告警与监控集成XXL-JOB 支持任务失败告警。在任务管理界面可以配置“报警邮件”支持多个邮箱用逗号分隔。当任务执行失败包括重试后依然失败时会自动发送邮件告警。对于更高级的监控可以关注xxl_job_log表中的失败记录将其接入公司的统一监控告警平台如 Prometheus AlertManager。可以写一个定时任务周期性扫描最近一段时间内失败的任务通过 Webhook 推送到钉钉、企业微信或飞书群。6.3 几个常见的“坑”与解决思路任务“执行中”但一直不结束在调度日志里看到任务状态一直是“运行中”但执行器日志没有更新。这通常是调度中心无法收到执行器的回调结果。检查网络连通性调度中心 - 执行器ip:port以及执行器任务代码是否发生了死循环或阻塞。执行器显示“离线”调度中心管理页面执行器地址显示为红色离线状态。首先检查执行器应用是否正常运行日志是否有报错。然后检查执行器配置的admin.addresses是否正确以及执行器到调度中心的网络是否通畅。在容器中特别要检查executor.ip是否配置正确这是注册成功的关键。任务被重复执行检查是否部署了多个调度中心实例且addresses配置错误导致每个调度中心都独立触发任务。确保它们使用同一个数据库并且执行器只配置了一个统一的接入地址VIP或负载均衡器地址。数据库连接数暴涨在高频任务或大量执行器的场景下调度中心和执行器都会频繁访问数据库。注意优化 MySQL 连接池配置并监控数据库压力。可以考虑对xxl_job_log表进行分库分表或定期归档。从我自己的经验来看XXL-JOB 的稳定运行90%的问题都出在网络配置和地址注册上。尤其是在微服务和容器化架构下把“调度中心如何找到执行器”以及“执行器如何上报自己”这两个网络通路理清楚整个系统就成功了一大半。剩下的就是根据业务特点合理设计任务的分片、路由和容错策略了。这个框架本身不复杂但把它稳稳地嵌入到你的技术架构中需要的就是对这些细节的把握。