ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

XXL-Job分布式定时任务在Spring Cloud微服务中的落地实践

XXL-Job分布式定时任务在Spring Cloud微服务中的落地实践 1. 项目概述先看清分布式定时任务这道题考什么XXL-Job 这个项目我断断续续用了快三年中间换过几个团队接手过的系统里有一个是从 Quartz 单体定时任务硬扛到分布式环境的越到后面越痛。所以这篇文章不打算给你抄一段 Hello World 就完事而是想从头把这个东西为什么存在、在什么架构场景下该用、实际接入时有哪些深水区讲清楚。整体围绕 XXL-Job 在 Spring Cloud 微服务架构中的落地展开把分布式定时任务从选型、部署、集成到治理的完整链路过一遍。先说结论XXL-Job 是一个开源的分布式任务调度平台核心思路是把“调度”和“执行”拆成两端。调度中心负责到点触发、记录日志、做路由策略执行器嵌在你的业务服务里接收调度的指令然后跑真实逻辑。它解决了我在微服务架构里最头疼的几个问题——定时任务重复执行、任务无法水平扩展、调度配置改了要重启服务、任务跑了不知道成功还是失败。这篇文章适合这几类人看第一正打算在 Spring Cloud 项目里引入分布式定时任务但不知道选什么方案、怎么接第二已经在用 Quartz 或者自己写死 Cron 做定时任务但节点一多就出现重复执行、单点压力大、任务日志难追踪第三想搞明白 XXL-Job 的 GLUE 模式和普通 Bean 模式差别搞懂路由策略、分片广播这些高级功能到底怎么落地。如果你只是需要一个在单机上跑跑的小工具那其实没必要上 XXL-Job一台机器一个 Cron 就够了但只要你开始面对多实例、多服务、任务量增长这篇文章应该能帮你省不少试错时间。2. 为什么需要“调度平台”自己写不行吗2.1 微服务化之后定时任务为什么变成了一坨乱麻我记得有一个真实的业务场景接到一个需求每天晚上两点清理三十天前的操作日志。单体时代这太简单了Spring 的Scheduled注解挂一个方法Cron 写死完事。但这个系统拆成微服务后服务部署了三台实例日志表也从业务库里独立出来按月份做了分表。这时候原来的思路全崩了。首先三台实例都会执行同一个清理任务一个表被三个进程同时删就算删之前都做了判断事务和锁的问题也会让日志表时不时出现死锁。我当时为了图省事用数据库行锁做了一版“分布式锁”任务启动先抢锁抢到才执行。但这玩意儿有个很大的问题——如果任务执行到一半进程挂了锁来不及释放第二天任务就永远跑不起来了需要人工去数据库删锁记录非常弱智。其次任务越来越多之后没有一个地方能看到我的任务到底有哪些、上次跑成功没有。A 服务里藏了三个ScheduledB 服务里藏了五个C 服务是别人离职前写的用了 Linux Crontab 直接调接口。线上出问题你根本不知道去哪里看。那一刻我就意识到定时任务的痛点根本不是“到点触发”这一步而是“发不发生、谁来执行、执行结果如何、失败怎么处理”这一整套治理逻辑。这才是 XXL-Job 这类调度平台真正的价值所在。2.2 技术选型Quartz、ElasticJob 和 XXL-Job 的横向对比当时的选型挺有代表性候选主要就是三个自研基于 Quartz 的集群方案、ElasticJob、XXL-Job。自研这事最诱惑人因为业务团队总觉得自己的需求最特殊市面上的框架不可能面面俱到。但我跟上个团队一起写过一次之后就不想再重复了你确实可以做 Quartz 集群部署加一张调度锁表让多个节点竞争触发权但你会发现自己开始写 UI、写日志存储、写失败重试策略、写路由算法。这些东西每个都看上去有点简单但越写越深最后占用的研发时间远超业务代码本身。ElasticJob 是当当开源的项目后来捐给了 Apache。它的强项是分片能力强但它的运维界面偏弱而且接入方式和 Spring 生态契合度不是那么顺滑。XXL-Job 的优势是轻量、UI 开箱即用、学习成本极低不需要额外引入 MQ 或者 ZookeeperElasticJob 依赖 ZK 的老版本方案调度中心是一个 Java Web 应用执行器只需要引入一个 SDK。在当时“快、稳、轻、有界面”这个诉求下XXL-Job 几乎是最合适的。另外很多人忽略的是 XXL-Job 的社区活跃度和资料密度。你去搜“分布式定时任务”十篇文章里有八篇是 XXL-Job 相关的这意味着踩坑基本都有现成的答案。技术选型这东西生态热度是很重要的参考指标冷门框架再牛出了问题没人帮你排那也是给自己埋雷。对比维度Quartz 自研集群方案ElasticJobXXL-Job调度模型依赖数据库锁竞争任务分片为主调度中心集中触发 执行器回调UI 界面需要自己开发较弱完善报表和日志都齐全动态调整参数需要改代码/配置支持但操作一般支持界面直接改 Cron 和参数路由策略自己写基于分片内置 10 种以上策略额外依赖数据库Zookeeper 或 Nacos仅数据库MySQL 即可上手成本高中很低3. 核心架构拆解调度中心和执行器到底怎么协作3.1 调度中心、执行器、任务、日志的关系XXL-Job 的架构图你去看官方文档会有一张挺清楚的设计图我这里不贴图用更直接的大白话讲三者的协作关系。调度中心是一个独立部署的 Web 服务它负责任务管理、Cron 触发、日志查询、告警推送。触发动作发生后调度中心会把“请执行 XX 任务、参数是 YY”这个消息通过 HTTP 请求发送给执行器。执行器是你业务系统里嵌入的一个组件它启动的时候会自动向调度中心注册——告诉调度中心“我是哪个应用、我有哪些 JobHandler、我在哪个 IP 和端口上”。这里有一个容易误解的地方调度中心和执行器之间不是长连接而是执行器主动注册 调度中心主动拉取/调用的模式。执行器启动时通过admin.addresses配置的地址找到调度中心把自己的信息上报。调度中心在执行任务时会根据配置的路由策略挑选一个执行器的地址然后发起 HTTP 调用。执行器执行完逻辑后把执行结果、日志通过 HTTP 回调给调度中心调度中心再把日志落库这样你在管理界面上就能看到完整的过程日志。这种调度和执行分离的设计本质上就是把“什么时候干活”和“谁去干活”解耦了。调度中心可以水平扩展做集群执行器也可以随时加机器而不用改调度配置。只要执行器的 AppName 一致新加的实例会自动被调度中心感知分片广播时也会自动参与进来。这就是分布式定时任务和单体 Cron 最大的区别横向扩展变得非常自然。3.2 调度中心需要数据库别忽略那几张表XXL-Job 调度中心是需要数据库的用它来存储任务配置、调度日志、执行器注册信息等。你拿到发行包之后目录下会有一个tables_xxl_job.sql在 MySQL 里执行一下建出来的表大概十几张。最早我搭环境时懒得看这些表结构直到有一次排查调度日志丢失问题才去翻了下数据表。建议你至少关注几张核心表xxl_job_info存任务定义xxl_job_log存调度日志xxl_job_registry存执行器实时注册信息xxl_job_group存执行器分组配置。理解这几个基本概念后很多问题排查起来会很有方向感。需要注意的一点调度中心本身是无状态的多台调度中心实例共享同一个数据库就可以组成集群。如果你们公司对可用性要求比较高调度中心至少要部署两台前面加个负载均衡数据库用主从或者云数据库的高可用版本。XXL-Job 官方文档明确写了调度中心支持集群部署也推荐这么做。但如果你只是几台执行器的小规模场景单机调度中心其实也够用因为调度中心本身的负载压力并不大真正的执行压力都在执行器那边。4. 从 0 到 1 的落地实操我接入 XXL-Job 的完整过程4.1 第一步初始化数据库和部署调度中心去 Gitee 搜 XXL-Job 官方仓库下载发行版源码或者直接下载 release 包都可以。我习惯直接用 release 包省去自己打包的麻烦。解压后在doc/db目录下找到tables_xxl_job.sql先建库再导入。表是给调度中心用的所以库的权限按调度中心的使用范围来收不用给业务服务开这个库的权限。调度中心本身是一个 Spring Boot 项目application.properties里主要改三样服务端口默认 8080如果和你们现有系统冲突就改掉数据库连接串和账号密码邮件告警相关的配置可以先用默认值后面再加。启动方式就是普通的java -jar或者打成镜像扔到 K8s 里跑也行。启动完成后浏览器访问http://调度中心IP:端口/xxl-job-admin默认管理员账号是admin/123456。界面长得很传统但功能确实齐全属于那种第一眼不惊艳、用起来很顺手的类型。4.2 第二步Spring Cloud 服务里接入执行器接入执行器前有一个前置动作你需要先去调度中心的管理界面创建一个“执行器管理”配置。执行器的 AppName 建议和你的服务名保持一致这样一看就知道是哪个服务。注册方式我推荐用“自动注册”让执行器启动后自己上报 IP而不是用“手动录入”去维护一个机器地址列表。自动注册在网络环境稳定的情况下非常省心后面执行器扩容都不用改任何东西。Maven 依赖很简单在业务服务里引入xxl-job-core版本要和调度中心版本保持一致否则可能出现执行器注册不上去、协议不兼容的问题。我踩过一次 2.3.0 的执行器对接 2.4.0 的调度中心表面看没报错但调度日志一直显示失败最后排查半天发现是版本不一致导致的从那以后我就强制要求两边版本对齐。依赖加好后写一个配置类把 XxlJobSpringExecutor 注册为 Spring Bean配置项如下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:9999}) private int port; Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); executor.setAccessToken(accessToken); executor.setAppname(appname); executor.setPort(port); return executor; } }对应的application.yml配置大概是xxl: job: admin: addresses: http://xxl-job-admin-server:8080/xxl-job-admin accessToken: default_token executor: appname: order-service port: 9999 logpath: /data/applogs/xxl-job/jobhandler有几个细节容易翻车这里单独说一下。第一accessToken 必须调度中心和执行器保持一致否则注册会被拒绝日志里会有很明显的提示。第二执行器会占用一个端口这个端口在你微服务 K8s 部署时要保证没被占用而且有时候需要添加防火墙规则。第三logpath 路径一定要确保服务进程有写权限不然任务执行成功但日志写不进去排查问题时会非常痛苦。4.3 第三步写一个任务把链路彻底跑通执行器接好之后写一个最简任务类。在方法上面加XxlJob注解注解值就是这个任务在调度中心里的标识通常叫 JobHandler。方法签名是固定的接收一个 String 参数这个参数对应创建任务时你在调度中心填的“任务参数”。Component public class DemoJobHandler { XxlJob(demoJobHandler) public void demoJobHandler(String param) throws Exception { XxlJobHelper.log(demo job start, param: {}, param); System.out.println(XXL-Job demo run, param param); XxlJobHelper.log(demo job success); // 任务执行成功后如果想让调度中心认定失败可以调用 XxlJobHelper.handleFail(reason) } }回到调度中心界面新建一个任务。任务配置这一栏有些关键选项值得认真理解。首先是 Cron这里用的是标准 6 位或 7 位 Cron 表达式直接在界面上填不用重启。运行模式选 “Bean”JobHandler 填demoJobHandler。路由策略一开始选“第一个”就行等后面理解了各个策略再调整。阻塞处理策略选“单机串行”意思是同一时间同一个任务实例只允许一个在跑后续触发的调度排队等待。新建完后点击“执行一次”如果调度中心和执行器的日志都一切正常调度中心的任务日志里应该能看到执行成功记录。如果失败第一步去看“调度日志”那一段提示第二步去执行器的 logpath 目录下看具体堆栈。整个链路第一次跑通之后你会对这套调度模型有非常直观的感受。5. 任务扩展玩法GLUE 模式、分片广播、动态参数与路由选择5.1 GLUE 模式改代码不发布的“线上编辑器”到底靠不靠谱XXL-Job 的 GLUE 模式是很有特点的一个东西也是网上搜热词“xxl-job glue方式”时被问得最多的功能。普通 Bean 模式需要你把任务代码写进业务服务里编译、发布、重启改动一次任务逻辑就发一次版。而 GLUE 模式把代码直接保存在调度中心的数据库里运行时把源码发给执行器执行器动态编译执行。这里的底层逻辑是 Java 的 Groovy 脚本引擎或者对 GLUE(Shell) 来说直接就是执行 Shell 命令。创建一个 GLUE(Java) 任务后调度中心界面上会出现一个“代码编辑器”你在里面写一个 Groovy 脚本写完保存后下次触发就会生效不需要业务服务重启。任务参数通过XxlJobHelper.getJobParam()获取日志同样用XxlJobHelper.log输出。这个模式适合什么场景我印象比较深的是一个运营系统里的奖励发放活动运营团队隔三差五改活动规则而且动不动就要第二天立刻生效。如果每次改规则都走一遍服务发版流程那效率完全跟不上。我用 GLUE 模式把规则脚本做成了可热更新的任务运营提出需求后开发同学在界面上改一段规则脚本点保存下一个 Cron 周期就执行新逻辑。这才是 GLUE 真正的用武之地。但 GLUE 也不适合过度使用如果任务逻辑很重、依赖了业务服务里的很多 Spring Bean那你得在脚本里自己想办法获取上下文代码反而会变得很难维护。而且出现问题的时候GLUE 任务不像 Bean 模式那样能直接看业务代码调试现场只能靠日志和回溯历史版本。我的经验是稳定的、复杂的、需要依赖业务底层服务的任务用 Bean 模式轻量的、频繁调整的、规则相对独立的逻辑用 GLUE。5.2 路由策略怎么看选错了会出现什么后果路由策略是任务触发时调度中心决定把请求发给哪个执行器的规则官方一共提供了好几种。从实际使用视角来看没必要把所有策略都研究透但常见几个的差异必须搞清楚。轮询和随机是最基本的负载均衡策略适合任务节点处理能力基本一致、任务本身不需要区分具体实例的情况。一致性 HASH 是按任务的 JobHandler 做 Hash 取模同一个任务永远落在同一个执行器上这对那些依赖了执行器本地状态的任务有一定意义。故障转移的逻辑是调度中心依次尝试各个执行器第一个能正常接收并执行的实例会抢到任务相当于自带了一层探活。忙碌转移则更谨慎它会先看执行器当前有没有在跑同名的任务如果有就把请求转发给下一个执行器能有效避免同一任务的并发冲突。我给你一个非常反直觉的例子我曾经在一个订单超时状态修改的任务上用了一致性 HASH因为当时想的是如果一个节点处理失败至少日志能集中在一个服务上方便排查。但实际运行一段时间后发现 Hash 到的那台实例负载越来越高因为所有任务都压在它上面其他实例空闲却分不到任务。后面换成了轮询并且额外加了失败重试整体负载立刻均衡了。路由策略没有绝对最优必须结合任务本身的特性来选择建议小流量下多试几种组合看效果。5.3 分片广播解决大数据量任务最常用的方案分片广播值得单独拿出来讲因为它是 XXL-Job 里做大数据量批量任务最核心的手段。我先描述一个真实痛点每天凌晨要统计前一天的订单数据生成报表和汇总。最开始我写了一个任务遍历整张订单表单台机器跑数据量上了千万之后这个任务从 20 分钟慢慢涨到三个小时仍然跑不完而且严重拖垮了业务数据库。分片广播的思路是把任务同时发给所有在线执行器每个执行器会拿到两个参数当前分片索引 index 和总分片数 total。假设你有 3 台执行器那总分片数 total3第一台机器拿到的 index0第二台 index1第三台 index2。各台机器只处理属于自己的那部分数据最后合并结果整体处理时间几乎能除以机器数量。在大数据量场景下怎么分片我在实践中发现最有效的是把主键 ID 对总分片数取模并行扫描时各机器互不干扰。举例来说订单表有一个id主键Excel 里先查一下当前数据的最小 ID 和最大 ID每台机器只查id % 3 index的数据就能保证数据不会重复也不会遗漏。如果数据不是数字主键比如按表做了分表那可以考虑按表名后缀取模这也是一种分片策略。分片广播有几个隐藏的坑。一是要注意“分片数变化”的兼容性比如今天 3 台机器执行分片总数是 3明天加了 1 台变成 4那按id % 3的旧任务 id 如果还留在某一张表中就会发生重复。所以数据拆分尽量不要依赖历史的分片策略。二是注意数据倾斜如果按一个业务字段取模某些字段的值可能明显多于其他字段导致不同执行器负载不均。最简单稳妥的做法还是按主键取模因为主键本身分布均匀几乎不会出现倾斜。5.4 失败重试与阻塞策略组合分布式定时任务里一个不那么显眼却经常出事的配置是“调度失败重试次数”和“任务阻塞处理策略”。直接聊我遇到的一个事故有个资费计算任务下游接口偶尔超时我设置了失败重试 3 次。结果有一次下游系统挂了 20 分钟这个任务每 5 分钟触发一次每个分片又重试 3 次导致同一个用户被重复发起多次计算请求最后把下游资源池打满了。痛定思痛后来我形成了一套自己的配置习惯。对于下游系统能力较弱、接口不是幂等的任务关闭失败自动重试或者只在确认具备幂等性的前提下开启重试次数限制在 1 到 2 次。对于幂等性好的任务比如“把状态从 A 改为 B”这种更新操作可以放心设置 2 次重试因为即使重复执行也不会产生坏结果。阻塞策略上如果任务是定时扫描型建议用“单机串行”如果任务发生重叠会产生严重问题就选择“丢弃后续调度”宁可少执行也不能并发如果任务允许覆盖比如清理过期数据可以用“覆盖之前调度”保证新的调度会终止旧的执行并重新开始。6. 线上实践与问题排查那些真正提升任务稳定性的硬经验6.1 调度中心日志看什么执行器本地日志怎么查很多人在用 XXL-Job 的时候看日志只看调度中心的“调度日志”页其实这里的信息量是有限的。调度日志分为两个大阶段调度阶段和执行阶段。调度阶段展示调度中心是否成功把请求发出去、发给了哪个执行器执行阶段展示执行器是否成功处理、返回什么结果。如果显示“调度成功”但“执行失败”问题大概率出在执行器端的业务代码如果显示“调度失败”问题就出在调度中心到执行器的通信链路或者执行器注册上。对于执行器内部的日志XXL-Job 支持两种落盘方式一是XxlJobHelper.log()记录的日志会上报到调度中心你在管理页面就能看到二是执行器自身应用的日志例如 Logback 输出的日志需要在执行器节点的本地文件查看。建议在任务代码里关键节点多打XxlJobHelper.log()方便出问题时在调度中心直接看到业务阶段的进展而不需要登服务器翻文件。6.2 生产环境里我踩过的几个坑问题现象根本原因解决办法新加的执行器机器一直显示注册不上执行器配置的 AppName 与调度中心里的执行器配置不一致核对两边 AppName自动注册模式下必须完全一致执行器显示在线但任务调度失败accessToken 不一致或执行器端口被防火墙拦截网络层排查端口连通性配置层 check token任务执行到一半主动结束执行器进程内有异常被全局捕获导致回调状态不正确检查代码是否吞掉异常关键逻辑用 try/finally 确保调用 handleSuccess 或 handleFail任务隔一段时间不执行重新点击执行一次又正常Cron 表达式没生效或用了非标准 Cron 位XXL-Job 默认 Cron 是 6 位或 7 位确认秒、分、时、日、月、周字段注意周与日的互斥表达多个节点同时执行了同一个任务路由策略选成了“第一个”但配置了失败重试导致重试时换了机器检查路由策略与重试参数开启“忙碌转移”降低并发概率我遇到过最隐蔽的一个问题发生在 K8s 环境里。执行器服务以 Pod 形式启动Pod 的 IP 每次重启都会变这本身不是问题——XXL-Job 支持自动注册IP 变了重新注册就行。但我们的调度中心在另一套网络里Pod 的 IP 是集群内网地址调度中心所在的机器访问不到这个网段导致每次 Pod 重建后调度中心里显示执行器注册成功真正触发却总是失败。最后给执行器服务配置了 NodePort 类型的 Service并将执行器的 IP 和端口手动指到了节点 IP 的映射端口上才彻底解决。这个问题在容器化部署时非常典型建议调试时期先确认调度中心能 ping 通执行器 IP 的对应端口。6.3 任务幂等与分布式调度场景下的兜底策略不管调度平台做得再好分布式环境下“重复执行”都是一个不能完全避免的现实。原因很简单网络超时后调度中心不确定执行器是否真的执行了很可能就重试了执行器执行了任务但回调调度中心时网络断了调度中心就会判定失败并触发第二次执行。所以业务代码一定要对任务的幂等性做兜底不要幻想调度平台能 100% 消灭重复。我在项目里常用的兜底策略是分布式锁加去重表。任务执行时先尝试获取一个 Redis 分布式锁key job:lock: 任务名拿到锁并且锁的 value 与本次调度 ID 对应才继续执行。任务结果写去重表成功记录任务调度的唯一 ID下次触发时先查这个 ID 是否已处理如果已处理直接跳过。这套兜底虽然比 XXL-Job 自带的阻塞策略稍微复杂一点但对关键资金类、账号类任务的保护作用非常大。6.4 怎么给任务加监控做到出了问题第一个知道任务调度平台本身只是执行不代表你不需要额外监视。我在团队里做了一个约定所有生产环境的 XXL-Job 任务在任务执行结束的回调方法里如果失败除了 XXL-Job 自带的告警功能外还要向公司的告警平台发一条事件。XXL-Job 自带的告警配置里可以设置邮件或者企业内部 IM 机器人但我建议你把失败任务和失败原因打点成一个 Metrics然后接到 Prometheus Alertmanager这样可以在任务连续失败 N 次或者任务执行时长超过阈值时自动报警。我个人的经验组合是XXL-Job 负责调度和触发外挂一套基础监控用于“任务不触发、触发异常、执行时间过长”这类比单纯失败更危险的场景。因为很多时候任务没有报错但就是没跑比如调度中心自己挂了这时执行器不会收到任何任务业务完全不感知只有靠监控探测调度日志表里最近一次记录的生长情况才能发现。每 10 分钟去查一下xxl_job_log表的最近调度时间如果超过一个合理窗口没有新记录就触发告警这个小脚本很土但真的救过我一次。我在实际使用中还发现一个容易被忽略的点任务的日志保留时间。XXL-Job 调度中心默认会保留一定时期内的调度日志数据量大了之后清理任务很有必要。官方提供了日志清理功能建议把它配成一个低频定时任务每周跑一次保留最近三个月日志即可。别让日志表无限增长否则调度中心的查询接口会越来越慢最终影响整个平台的稳定性。最后再分享一点自己的心得接入 XXL-Job 之后团队里最容易犯的错误就是把所有业务逻辑都塞进任务然后只依赖界面上那几个按钮忽略了任务代码本身的性能。实际运行中一个任务一次能处理多少数据、数据库连接怎么控制、失败退回多少这些问题直接影响线上稳定性。调度平台只是给你搭好了舞台怎么把戏演好最终还是业务代码说了算。建议第一次接入时先从 1 到 2 个核心任务开始小范围试用观测两到四周确认稳定再逐步扩大任务范围这样能把风险限制在可控的范围内。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进