ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot集成xxl-job:从零搭建分布式任务调度中心与执行器

SpringBoot集成xxl-job:从零搭建分布式任务调度中心与执行器 先说一下为什么会写这篇东西。定时任务这东西做后端的基本都绕不过去从最早直接用Thread.sleep循环、到Spring自带的Scheduled再到后来单机扛不住、引入分布式调度每一步都是被业务逼出来的。我大概从xxl-job还叫xxl-job-2.0那会儿就开始用中间换过Quartz、也试过别的平台兜兜转转最后还是把大部分项目的定时任务都收敛到了xxl-job上。原因后面细说先给你看结论如果你需要在SpringBoot项目里做一套可靠、可视、支持分布式部署的定时任务调度xxl-job是目前综合成本最低的选择之一。这篇文章会把服务端搭建、客户端集成、任务配置、运行模式、常见坑一次性讲完照着做基本不会翻车。1. 先搞清楚为什么是xxl-job而不是Quartz或Spring自带的定时任务1.1 定时任务在业务里的常见痛点很多项目一开始用Scheduled写起来确实爽一行注解就完事。但等业务跑起来问题就一个个冒出来了任务跑挂了没有任何通知日志沉底经常是业务方发现数据不对了才知道任务早停了;任务执行时间不固定想临时手动触发一次得写接口或者改代码重启;多实例部署时任务会重复执行要么加分布式锁要么单独起一个实例专门跑调度很笨重;任务执行耗时、成功失败情况全靠日志没有一个直观的看板;任务多了以后代码里散落一堆定时任务没人说得清线上到底跑了哪些、什么频率、什么参数。这些问题在小项目里还能忍一旦业务量上来任何一个都能让你半夜爬起来查日志。Quartz能解决一部分问题但它本身没有UI分布式部署也要自己写扩展学习成本不算低。1.2 xxl-job核心优势拆解xxl-job之所以被这么多项目采用我认为核心是它直接命中上面这些痛点可视化控制台任务的增删改查、暂停/启动、手动触发、日志查看全在浏览器里操作不用改代码;分布式调度调度中心支持集群部署执行器支持水平扩展任务在执行器之间自动分发;运行模式丰富支持Bean模式、GLUE模式在线编写代码、脚本模式等灵活度高;自带路由策略第一个、最后一个、轮询、随机、故障转移、分片广播等能覆盖绝大多数分发需求;失败处理机制支持调度失败重试、执行失败告警还预留了邮件告警通道;国内社区活跃网上踩坑文章多遇到问题基本搜得到解决方案。一句话总结xxl-job把定时任务从“裸奔”变成了“有管理后台、有监控、可动态调整”的完整方案。1.3 整体架构和核心概念梳理在动手搭建之前有几个概念必须先理清楚不然后面配置起来容易懵调度中心Admin一个独立的Web应用负责任务的注册、调度、日志管理。可以简单理解成“大脑”;执行器Executor跑实际业务代码的应用也就是你的SpringBoot项目。它启动后会自动注册到调度中心可以理解成“手脚”;任务在调度中心里配置的一条“什么时间执行什么方法”的记录;调度到了配置的时间点调度中心向执行器发送HTTP请求触发执行器执行对应的方法。这里面最重要的一个点是调度中心和执行器是通过HTTP接口通信的不是RPC也不是消息队列。所以执行器不一定是Java项目理论上任何语言只要实现xxl-job的协议都行。这也是xxl-job扩展性强的一个原因。2. 服务端搭建从源码到能跑起来的完整过程2.1 环境准备清单xxl-job调度中心本身是一个SpringBoot项目所以环境要求其实不高JDK 1.8 / 8我用的是JDK 8稳妥新版可尝试11或17但没太大必要;Maven 3.6用于编译源码;MySQL 5.7需要初始化数据库用5.7或8.0都行;一个顺手的IDEIDEA或Eclipse均可。我用的是2.4.1版本官方推荐的稳定版后面的步骤也基本以这个版本为例。如果你用更新的版本配置项可能会略有差异但整体思路一致。2.2 源码下载和编译打包xxl-job源码在GitHub上开源直接clone下来git clone https://github.com/xuxueli/xxl-job.git如果你不需要改源码其实编译整个项目有点浪费时间。建议直接进入xxl-job-admin子目录模块单独打包cd xxl-job/xxl-job-admin mvn clean package -DskipTests打包完成后target目录下会生成一个xxl-job-admin-2.4.1-SNAPSHOT.jar。这个Jar就是调度中心可以直接用。提示如果Maven依赖拉取比较慢建议给仓库配置阿里云镜像不然编译过程能急死人。2.3 初始化数据库xxl-job需要有一张数据库来存任务配置、调度日志、执行器注册信息等。源码里自带建表脚本你需要手动执行一下。脚本位置/xxl-job/doc/db/tables_xxl_job.sql用Navicat或命令行执行即可mysql -uroot -p tables_xxl_job.sql执行完会在MySQL里创建一个名为xxl_job的库。默认有一张未命名的配置表xxl_job_group以及任务表、日志表、锁表等若干张。建议先用默认库名能少改很多配置。2.4 修改配置文件并启动进入xxl-job-admin的src/main/resources目录找到application.properties核心配置项如下server.port8080 spring.datasource.urljdbc:mysql://127.0.0.1:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password123456需要改成你自己MySQL的连接信息。如果你改了库名这里也要对应改。其他配置项默认就行等熟悉了再去调。然后启动java -jar target/xxl-job-admin-2.4.1-SNAPSHOT.jar看到日志里出现“启动成功”字样就可以访问了。浏览器打开http://localhost:8080/xxl-job-admin默认账号密码是admin/123456。2.5 初次登录与基础配置登录后先别急着配任务有几个地方我建议你先看一下执行器管理这里会显示所有接入的SpringBoot应用启动执行器后会自动注册在这里;调度日志所有任务调度的执行记录都在这里排查问题最主要看这个;GLUE编辑器如果你打算用在线代码模式任务详情页里就能进入编辑器后面细说。到这里调度中心的搭建就完成了。接下来是最关键的一步——怎么把SpringBoot项目接入进来把业务方法暴露成可调度的任务。3. SpringBoot客户端集成一步步把任务跑起来这一部分是重点集成步骤非常多网上很多教程在客户端这块都写得过于简略我这里从头到尾过一遍。3.1 引入依赖在你的SpringBoot项目的pom.xml中加入xxl-job的依赖dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.1/version /dependency3.2 配置application.yml在SpringBoot项目的application.yml里增加执行器相关配置xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin accessToken: default_token executor: appname: my-springboot-executor address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30逐个解释一下这几个配置项的含义admin.addresses调度中心地址多个地址用逗号分隔;accessToken通信令牌调度中心和执行器两边要一致否则通信会被拒绝。默认是default_token如果你在调度中心配了别的这里必须改成一致;executor.appname执行器名称在调度中心注册时用的名字后面添加执行器时要对应;executor.port执行器端口用于接收调度中心的HTTP请求。注意不要和SpringBoot项目本身的端口冲突;executor.logpath任务执行日志的存放路径这个目录要存在且有写权限;executor.logretentiondays日志保留天数默认30天自动清理。3.3 配置XxlJobSpringExecutor在SpringBoot启动类或者任意Configuration类里注册XxlJobSpringExecutor的BeanConfiguration 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; Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor xxlJobSpringExecutor new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); return xxlJobSpringExecutor; } }这里有个小坑如果你不想用配置文件里的值也可以直接在代码里写死但不推荐。因为环境不同本地、测试、生产调度中心地址和令牌很可能不一样写成配置方便部署管理。3.4 编写第一个任务Handler在SpringBoot项目中新建一个类写一个方法加上XxlJob注解这个方法就变成了一个可被调度中心调度的任务Component public class SampleXxlJob { private static final Logger logger LoggerFactory.getLogger(SampleXxlJob.class); XxlJob(demoJobHandler) public void demoJobHandler() throws Exception { logger.info( xxl-job demo task started); System.out.println(hello xxl-job, this is a demo task); logger.info( xxl-job demo task finished); } }注意三点类必须被Spring容器管理也就是加上Component或Service注解否则执行器扫描不到;方法必须加XxlJob注解注解的值就是任务Handler名称后面在调度中心配置任务时要对应;方法必须是public void可以有参数也可以没有参数如果有参数传的是String类型可以接收调度中心传过来的JSON字符串。到这里SpringBoot端的代码已经写完了。启动项目如果一切正常日志里会看到xxl-job register executor success并且调度中心的“执行器管理”页面里会出现一个名为my-springboot-executor的执行器。3.5 在调度中心配置任务并执行调度中心配任务的过程很多人第一次会摸不着头脑。跟着下面步骤走在“执行器管理”页面确认执行器已注册。如果没有点“新增”手动添加一个AppName填my-springboot-executor名称随意;进入“任务管理”页面点“新增”;配置任务信息重点字段如下执行器下拉选择刚刚注册的执行器;任务描述写清楚这个任务是干嘛的;调度类型选Cron填一个Cron表达式比如每隔30秒执行一次就填0/30 * * * ? *;运行模式选Bean;JobHandler填demoJobHandler和代码里的注解名一致;阻塞处理策略选单机串行后面会细讲;路由策略如果只有一个执行器选第一个或轮询都行。保存后在任务列表里点击“操作”列的“启动”任务就开始按Cron调度了。如果想立即跑一次不等到时间点点“执行一次”按钮。去执行器项目的控制台看一下如果打印了hello xxl-job, this is a demo task说明整条链路已经通了。到了这一步从服务端搭建到客户端集成的最小闭环已经完成了。4. 运行模式与调度策略这块搞明白了才算真正会用xxl-job很多人把任务配完能跑就觉得完事了但实际上对运行模式和调度策略的理解决定了你在复杂场景下能不能把xxl-job用好。这一章详细拆一遍。4.1 BEAN模式与GLUE模式的区别xxl-job的运行模式在配置任务时必选最常用的是这两种BEAN模式任务调度的是Spring容器里某个Bean的某个方法对应代码里用XxlJob注解标记的方法。这种方式的好处是业务逻辑在代码里有版本管理、可以单测适合正式项目;GLUE模式任务逻辑直接以代码片段形式存在调度中心里调度中心附带一个在线编辑IDE修改立即生效不用重新部署应用。适合临时任务、运营配置类任务、快速热更新场景。我个人的经验是正式任务全部用BEAN模式。GLUE模式虽然方便但代码在数据库里时间一长没人维护特别容易变成“黑盒”。曾经接手过一个项目调度中心里躺着十几个GLUE任务最早的一个是两年前的业务方都不记得是干嘛的了非常被动。4.2 路由策略怎么选路由策略是针对“有多个执行器实例都在跑同一个任务”时调度中心该把任务分发给哪个实例的问题。常用场景如下策略含义适用场景第一个/最后一个固定发给第一个/最后一个实例单活任务比如每天只跑一次的报表任务轮询多个实例依次轮流多活且无状态的任务比如批量发送短信随机随机选一个负载均衡要求不高的场景故障转移发一个失败的实例自动换另一个对实时性要求高的任务分片广播所有实例都执行带分片参数需要把数据分批处理的大批量任务重点说一下分片广播。这是xxl-job非常实用的一个策略比如你有100万条数据要批量处理部署了5个执行器实例选择分片广播后每个实例都会执行任务但会收到不同的分片参数shardIndex和shardTotal你可以根据当前是第几个实例/总共几个实例算出自己负责哪些数据从而实现分布式并行处理大大提升任务吞吐量。第一次接触分片概念可能有点绕举个例子假设清理临时文件5个实例分片广播每个实例通过XxlJobHelper.getShardIndex()拿到自己的编号0-4通过getShardTotal()拿到总实例数5然后只处理文件ID % 5 自己编号的那部分文件。这样100万个文件每个实例只用处理20万效率直接翻几倍。4.3 阻塞处理策略当任务调度时间到了但上一个任务还没执行完这时候会触发阻塞处理策略。三个选项的区别必须搞清楚单机串行新任务排队等上一个执行完才继续适合大多数场景;丢弃后续调度新任务来了发现上还没执行完直接丢适合对数据时效性不强、怕重复处理的任务;覆盖之前调度新任务开始执行时把上一个还没执行完的任务终止掉适合永远只需要保留最新一次执行结果的任务。实际项目里单机串行覆盖了90%的场景。覆盖之前调度用起来要小心因为强制终止线程可能导致资源没释放。5. 常见问题与排查思路实录我在实际项目中踩过的坑文档上的东西都好说真到出问题时才是最考验人的。这里把我这几年用xxl-job遇到的比较典型的问题列出来附上排查思路和解决办法。5.1 调度中心启动失败现象执行java -jar启动xxl-job-admin日志报一堆SQL异常或数据源连不上。分析大概率是MySQL连接信息配置不对或者没执行建表脚本。xxl-job启动时要访问数据库如果表不存在会初始化失败但它不会自动建表必须先手动执行SQL脚本。解决办法确认application.properties里的数据库URL、用户名、密码正确;确认xxl_job库是否存在登录MySQL执行show databases;看看;如果库不存在或表不全重新执行tables_xxl_job.sql。5.2 执行器启动成功但调度中心看不到现象SpringBoot项目启动正常日志也显示注册成功但调度中心的“执行器管理”页面就是没有新的执行器。分析最常见的原因是执行器管理里没有对应AppName的执行器配置。调度中心的执行器列表是需要手动添加的不会自动出现。解决办法在“执行器管理”页面点“新增”AppName填和application.yml里xxl.job.executor.appname一致的值比如my-springboot-executor保存后再刷新页面就能看到执行器了状态会是“已注册”。另一个原因是accessToken两边不一致日志里会提示JobController receive job handler error之类的问题去检查调度中心和客户端的Token是否一致。5.3 任务触发成功但业务没执行现象调度中心日志显示任务“调度成功”但执行器应用的日志里没有对应打印。分析这个问题比较隐蔽大多数情况是JobHandler名字对不上。调度中心配置任务时JobHandler字段填的名字必须和代码里XxlJob(xxxx)注解的值完全一致大小写、空格都要一致。还有一个容易忽略的点注册的是多个执行器路由策略选的第一个但第一个实例可能不是你的本地实例所以看起来像是“没执行”。解决办法先在调度日志里看“执行成功”的机器IP是多少再到那台机器上看日志。如果是JobHandler名字问题把调度中心的配置值和代码注解值改成一致即可。5.4 任务执行超时或被误杀现象任务跑了很久调度中心显示执行失败但应用日志显示任务还在继续执行。分析xxl-job默认有一个超时时间30秒超过这个时间还没执行完调度中心会记录失败。但实际业务场景里很多批量处理任务都要跑几分钟甚至更久。解决办法两个方向。一是如果业务允许把任务拆小用分片广播并行处理二是在xxl-job任务配置页面调大“超时时间”参数单位是秒可以设成0表示不设超时。另外注意长时间执行的任务最好用异步处理或独立线程池尽量避免阻塞执行器的业务线程因为执行器本身还要响应其他任务的调度请求。5.5 线上问题速查表这里整理一份我平时排查问题用的速查表照着顺序检查大多数问题都能定位症状第一步检查第二步检查常用解决办法调度中心登录不上端口是否被占用MySQL是否连通改端口确认数据库配置执行器日志没有输出检查执行器路径有无权限检查logback配置给目录加权限或改路径任务调度不触发Cron表达式是否正确任务是否“启动”状态用在线Cron生成器校验任务执行重复路由策略是否选了广播是否用了统一的分布式锁改用单机串行日志报权限异常执行器用户名是否文配置调度中心是否加白名单使用默认token5.6 日志查看与定位技巧xxl-job的日志分两层调度中心日志和执行器日志。调度中心日志在“调度日志”页面里可查看可以看到任务的触发时间、执行结果、调度耗时、执行耗时。如果任务执行失败点“日志”按钮会跳到执行器里的实际日志文件这是定位问题的主入口。执行器日志就是应用日志但xxl-job把每次任务执行记录在executor.logpath配置的目录下文件名带JobId和执行时间非常清晰。重点提醒一下如果执行器跑在容器或者K8s里logpath要配置成持久化目录不然Pod重启日志就丢了很难排查历史问题。6. 从demo到生产环境一些值得改进的细节如果你按前面的步骤把Demo跑通了下面这些细节是从“能用”到“好用”的关键。6.1 调度中心高可用集群部署生产环境调度中心不能是单点否则调度中心挂了任务全部停摆。xxl-job官方支持调度中心集群部署做法很简单把打包好的Jar在多台机器上分别启动;数据库共用一个MySQL库;通过Nginx或负载均衡把请求分发给多个调度中心节点。注意调度中心集群要求数据库必须共用因为所有节点的状态都存在数据库里。另外调度中心分别启动后执行器端的admin.addresses配多个地址即可比如xxl: job: admin: addresses: http://192.168.1.10:8080/xxl-job-admin,http://192.168.1.11:8080/xxl-job-admin6.2 执行器水平扩展与优雅停机执行器部署多实例后调度中心会自动在多个实例间做负载分发。但这里踩过一个大坑执行器是临时注册的默认30秒上报一次心跳如果实例宕机调度中心最多要等30秒才能感知并摘除节点。这期间调用方可能会调到已宕机的实例上。解决办法配置好心跳时间xxl.job.executor.registry-interval和优雅停机。SpringBoot项目的优雅停机配置如下spring: lifecycle: timeout-per-shutdown-phase: 30s同时在执行器关闭前手动注销节点PreDestroy public void destroy() { xxlJobExecutor().destroy(); }这样在发布重启的时候调度中心能较快感知实例下线减少任务分发到“将死”实例的概率。6.3 动态参数传递让定时任务更灵活xxl-job支持在任务配置时填写“任务参数”这个参数会以JSON字符串的形式传给XxlJob注解方法的入参。比如你可以给同一套代码配置两条任务任务A参数{type:invoice,date:2024-01-01}任务B参数{type:receipt,date:2024-01-01}代码里通过XxlJobHelper.getJobParam()获取参数然后解析JSON做对应的业务处理。这样一条写死的任务逻辑因为参数不同可以复用大大提高了任务的可配置性。实际业务里我经常用这个功能来做“补数据”场景。不需要改代码直接在调度中心手动触发一次带上指定日期参数就把某一天的数据重算了一遍。这个功能在运营活动、数据修复时尤其好用。6.4 任务监控与告警别等业务方来找你xxl-job虽然自带告警接口但默认需要自己实现JobAlarmer接口或者配置邮件告警。我这里更推荐的做法是把xxl-job的调度结果主动同步到你们自己现有的监控体系。具体做法不复杂在执行器的任务方法里把执行结果、耗时写到日志;用日志采集工具比如ELK、Loki收集执行器日志;配置关键字告警比如“ERROR”“Task Failed”等。如果你没有现成的监控体系可以先从最简单的方式做起写一个定时任务也可以用xxl-job本身定期扫描调度中心的失败日志表发现失败任务就往钉钉/飞书群里发通知。等业务规模再大一些再考虑接入专业监控平台。7. SpringBoot集成xxl-job的常见问题快问快答结合搜索热词把平时群里大家问得比较多的问题也一并回答了。7.1 SpringBoot2.x和SpringBoot3.x兼容吗官方说明是支持到SpringBoot2.x为主SpringBoot3.x需要自己适配主要是javax到jakarta的包路径切换问题因为xxl-job-core内部用到了Servlet API。目前2.4.x版本对SpringBoot3.x的兼容不算特别好新项目如果用SpringBoot3建议先升级试试确认没有兼容问题再用。7.2 Scheduled和xxl-job能同时用吗可以同时使用。但一般不建议同一个项目里两种方式混用因为维护上容易混乱。如果你已经用了xxl-job建议逐步把Scheduled迁移过去统一管理入口。7.3 xxl-job和Quartz怎么选如果项目规模小、任务量不大、也不需要可视化管理Scheduled和Quartz就够了。但只要任务超过10个、或者需要多实例部署、或者运营和产品需要经常手动触发任务直接用xxl-job最省心。Quartz本身是个好框架但运维和可观测性这块确实是xxl-job的强项。7.4 任务执行失败会重试吗xxl-job默认失败不会重试需要手动在任务配置里设置“失败重试次数”。注意这个重试是调度中心重新触发一次任务不是执行器内部重试。如果业务对失败重试有特殊要求比如需要退避建议在代码里自己实现。7.5 执行器是SpringBoot项目但不想启动Web服务器怎么办xxl-job的执行器本质上是一个内嵌的Netty HTTP服务和SpringBoot的Web容器是分开的。如果项目里不需要Web功能可以在pom里排除自带的Web依赖但执行器端口依然会监听不影响任务调度。7.6 怎么保证任务在集群下不重复执行两种情况区分一是选择路由策略为“第一个”或“随机”这种天然只有一个实例执行不存在重复二是选“分片广播”那每个实例执行的数据范围必须按分片参数隔离代码里要有% shardTotal之类的逻辑否则重复是必然的。我个人在这个问题上补一刀不管路由策略是什么任务处理的核心步骤尽量做幂等。比如用数据库唯一索引、Redis锁即使极端情况下重复触发也不会产生脏数据。这个习惯能让你省掉很多线上事故。8. 从搭建到运维的经验之谈文章写到这里该讲的步骤和技术点都讲完了。最后分享一点我个人的体会。刚开始用xxl-job的时候我其实有点“嫌弃”它只是把Quartz包了一层外壳但实际用久了才意识到定时任务这一层真正的难点不是“触发”而是“管理”和“可观测”。xxl-job的价值恰恰体现在这里——它让你对线上所有定时任务的状态、执行记录、成功率一目了然而这一点在业务复杂度上来之后几乎成了刚需。从一个中型项目的角度看xxl-job的服务端部署一次之后基本不用管平时主要工作是执行器集成和新任务的配置这些都只需要在控制台点几下就能完成对开发者的技术负担非常小。最后分享两个小技巧。一个是任务命名规范建议格式为业务模块_动作_描述_日期比如order_statistics_daily_2024这样在任务列表里一眼就能看出是哪个业务、做什么、什么频率排查问题时体验完全不一样。另一个是拿到新项目先看调度中心的任务列表这比翻代码更快了解项目跑着哪些定时任务尤其是接手老项目的时候这一步能帮你快速摸清业务里有哪些“定时动作”。希望这篇文章能让你把xxl-job顺利跑起来少踩一些我当年踩过的坑。后面有时间的话我打算再写一篇关于xxl-job二次开发的内容比如自定义告警通道、扩展执行器的鉴权逻辑欢迎持续关注。
返回列表