XXL-JOB实战:从原理到Demo搭建,搞定数据中台报表自动汇总
发布时间:2026/10/10 7:53:33 锦皓数字建站

1. 从调度需求说起为什么我盯上了XXL-JOB先交代一下背景。我这边负责一套数据中台的底表维护工作每天凌晨要跑一大批离线统计任务其中最头疼的就是“每日报表自动汇总”——业务方每天早上一上班就要看到前一天的核心经营数据包括订单量、成交金额、新增用户、渠道转化等等。这些数据散落在十几张业务表里还要做去重、口径统一、异常剔除最后汇总成一张宽表再同步到数仓和报表平台。最开始这个活儿是拿服务器 crontab 脚本硬跑的写了一大堆 Shell 加 SQL 的拼接每天凌晨两点准时执行。表面上能用但问题非常明显业务方偶尔会临时要求补数据脚本一改就要重新上线风险高任务跑挂了没有告警第二天早上业务方发现报表是空的我们才知道出了问题多个任务之间如果存在依赖关系crontab 根本编排不了只能靠“在脚本里 sleep 半小时”这种土办法没有执行日志管理每次排查都要翻系统日志痛苦得一匹。后来我就琢磨着引一个调度框架进来。市面上的方案我大概对比过——Azkaban、Airflow、DolphinScheduler、XXL-JOB。选型的时候考虑到我们团队后端以 Java 为主、不想引入太重的大数据组件、需要运维简单可视化操作最终锁定了 XXL-JOB。我花了一个周末的时间拿一个 demo 把“每日报表自动汇总”这个场景完整跑通了。这篇文章就把我的整个实践过程拆开来讲先从 XXL-JOB 的核心原理说起再说怎么搭一个最小可运行的 demo然后讲报表任务在里面的完整落地流程最后是这段时间踩过的坑和我个人的一些建议。篇幅不短我尽量把每一个细节和为什么这么做都说清楚。2. 先花五分钟理解 XXL-JOB 的核心设计在动手写代码之前我强烈建议你先搞清楚 XXL-JOB 的工作原理。不然后面配置任务、排查问题的时候你会一头雾水。2.1 XXL-JOB 到底是什么XXL-JOB 是一个分布式的任务调度平台。它解决的核心问题是把“什么时候执行什么任务”这件事从业务代码里抽出来统一管理。打个比方你就明白了。你以前写定时任务就是在代码里设个闹钟闹钟响了就干活。问题是如果系统部署了好几台机器闹钟会响好几次同一个任务可能被重复执行。而且你改闹钟时间的时候必须重新编译、重新发布代码非常麻烦。而 XXL-JOB 相当于一个中心化的“指挥中心”它拿着所有任务的时间表到点了就打电话给对应的执行器说“该你干活了”。谁来干、什么时候干、干完了没中心都有记录。这套架构里有三个角色调度中心Admin Server负责管理任务、触发任务、记录日志、执行告警。它是一个独立的 Web 应用有可视化界面。执行器Executor业务项目集成 XXL-JOB 依赖后就变成了执行器。它负责接收调度中心的指令真正去跑你写的任务代码。任务Job你在执行器里编写的一个个方法被调度中心按策略触发。调度中心和执行器之间通过 HTTP 接口通信执行器启动后会主动注册到调度中心。所以你会发现XXL-JOB 对业务代码的侵入程度很低——你只需要在项目里引入一个依赖加上几行配置然后把一个方法标记成一个任务即可。2.2 调度中心和执行器的通信原理很多刚接触 XXL-JOB 的同学会有疑问为什么我启动项目后调度中心列表里还是看不到执行器关键在于“注册”这个动作。执行器启动时会根据配置的 admin 地址调用调度中心的注册接口把自己的 ip:port 和 AppName 上报上去。调度中心收到之后才把执行器在界面上标记为“在线”。所以如果你的执行器一直“离线”排查方向基本就两条执行器所在机器到调度中心的网络不通执行器应用名AppName跟调度中心配置的不一致。通信这块XXL-JOB 用的是自研的 HTTP 请求封装调度中心到执行器是“调度”执行器到调度中心是“注册”和“回调”。任务跑完后执行器会把执行结果回调给调度中心这样你在调度中心的日志页面就能看到成功还是失败。2.3 路由策略、阻塞处理、失败重试这三个概念是配置任务时一定会碰到的我简单展开说一下。路由策略决定了当一个任务有多个执行器节点时调度中心把任务分给谁。轮询轮流分发负载均衡第一个永远发给第一个注册的节点故障转移先检查节点健康状态只发给存活节点分片广播把所有节点都发一遍适合每个节点处理一部分数据比如按订单号取模。我们的报表汇总任务用的是“轮询”因为执行器是单节点部署轮询和第一个效果一样。阻塞处理策略决定了当任务还没跑完下一次触发时间又到了怎么办。单机串行同一个任务在同一个执行器节点上排队执行等上一个跑完再跑下一个丢弃后续调度新触发的直接放弃覆盖之前调度新触发的开始跑把旧的停掉。报表任务涉及长时间的数据计算绝对不能覆盖之前调度所以用的是“单机串行”。失败重试就比较直白了指任务失败后自动重新调度几次。这里提醒一句报表汇总这种任务重试前最好确认一下数据幂等性。比如如果你已经插了一半的数据重试又从头插一遍大概率会出重复数据。我后面会专门讲这个问题。3. 搭建最小可运行的 XXL-JOB Demo说再多原理都不如跑一个 demo 来得实在。我这一节就把搭建过程完整写出来照着做你也能跑起来。3.1 环境准备我本机用的环境是这样的JDK 1.8Maven 3.6MySQL 5.7一个干净的空项目Spring Boot 版本 2.xXXL-JOB 有两个版本分支老版本 2.3.x 和后续版本。新版本把调度中心的代码拆成了 xxl-job-admin 和 xxl-job-core 两个模块我直接用的 GitHub 上的 master 分支。数据库准备这一步很关键。你的 MySQL 里需要建一个数据库然后执行调度中心源码里的tables_xxl_job.sql脚本。这个脚本会创建大概十来张表包括任务信息表、执行器表、日志表、调度日志表、用户表等。提示我用的是 2.4.0 版本的脚本。不同版本的 XXL-JOB 表结构有差异不要随便拿网上贴的旧版脚本混用否则调度中心启动后会报表结构不匹配的错误。3.2 启动调度中心调度中心本身是一个 Spring Boot 应用。你需要修改它的配置文件application.properties把数据库地址改成你本地 MySQL 的连接串然后改一下调度中心的内置端口。我本地的配置是这样的server.port8080 spring.datasource.urljdbc:mysql://127.0.0.1:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8 spring.datasource.usernameroot spring.datasource.password你的密码改完直接运行XxlJobAdminApplication这个启动类。启动成功后访问http://localhost:8080/xxl-job默认账号密码是admin/123456登录进去就能看到调度中心的控制台。这个控制台就是你日常管理任务的地方新增任务、修改执行时间、查看执行日志、手动触发一次任务、查看调度报表都在这里操作。3.3 写一个最简单的执行器程序调度中心起来了只是个空壳真正干活的是执行器。我们新建一个 Spring Boot 项目引入 XXL-JOB 的 core 依赖dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.0/version /dependency然后在配置文件里加上执行器相关的配置xxl.job.admin.addresseshttp://127.0.0.1:8080/xxl-job xxl.job.accessTokendefault_token xxl.job.executor.appnamereport-executor xxl.job.executor.port9999接下来写一个配置类把 XxlJobSpringExecutor 这个 Bean 构建出来Configuration public class XxlJobConfig { Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor new XxlJobSpringExecutor(); executor.setAdminAddresses(http://127.0.0.1:8080/xxl-job); executor.setAppName(report-executor); executor.setIp(); executor.setPort(9999); executor.setAccessToken(default_token); executor.setLogPath(/data/logs/xxl-job); return executor; } }这里面有几点要注意setIp留空表示让执行器自动探测本机 IPsetPort一定不能跟本机其他端口冲突setLogPath是执行器保存任务执行日志的磁盘路径一定要确保目录存在且有写权限不然任务会报日志相关的错误。写好后启动这个 Spring Boot 项目注意观察启动日志。如果看到类似 “xxl-job executor init success” 之类的输出就说明执行器注册成功了。这时候回到调度中心控制台在“执行器管理”页面应该能看到刚刚启动的report-executor状态是“在线”。3.4 在调度中心注册第一个任务执行器在线了接下来就可以创建任务了。在调度中心控制台左侧菜单点“任务管理”选择执行器report-executor新增一个任务。任务配置里最关键的有几个任务名称随便起一个建议描述性强的比如“每日经营报表自动汇总”Cron填写触发表达式路由策略选轮询运行模式选 Bean 模式Glue 模式后面再说JobHandler填写你在执行器里定义的 handler 名称阻塞处理策略选单机串行失败重试次数根据场景设置。然后回到代码在业务方法上加上XxlJob注解即可Component public class ReportDailyJob { XxlJob(dailyReportJobHandler) public void dailyReportJobHandler() throws Exception { System.out.println(开始执行每日报表汇总任务); // 这里放真正的报表汇总逻辑 } }保存任务后回到任务管理页面点击“执行一次”按钮稍等几秒去日志页面看执行结果。如果日志里打印出了 “开始执行每日报表汇总任务”说明整个链路已经通了。到这里一个最小的 XXL-JOB demo 就跑通了。你动手试一试会发现从零到能手动跑通一个任务其实也就一个小时左右。4. 把每日报表汇总任务真正落到 XXL-JOB 上demo 通了以后就要开始干正事了。我前面说过原本的报表任务是靠 cron 脚本加 SQL 拼接完成的现在要把它改造成一个 XXL-JOB 的任务。这个改造过程我拆成四步走。4.1 数据准备与口径整理在做任何代码改造之前先把数据口径固定下来。这一步往往比写代码还重要。我们的日报汇总逻辑大概是这样的从业务订单表取昨日订单数据从用户表取昨日新增用户数从各渠道明细表取各渠道的转化数据排除测试订单、退款订单、内部刷单数据这些在表里都有标记字段最终按“日期渠道”维度聚合成一条记录写入报表宽表。因为原始表的数据量很大我建议不要直接在定时任务里写那种多表大 Join 的 SQL最好是先写几个独立的中间表再合并汇总。这样性能更好也方便问题排查。比如昨晚某渠道数据异常你可以直接查中间表定位是哪一步出了问题而不是在一大坨 SQL 里挣扎。4.2 在代码里实现任务的幂等幂等这个问题是我在这个项目里花时间最多的地方。假设你这段报表汇总任务在凌晨两点触发跑了一半突然数据库连接断了任务失败。调度中心按你配置的失败重试次数半小时后又触发了一次。这时候上次插入的一半数据还在表里重新跑一遍就会产生重复数据。我用的方案是“业务日期作为唯一键”表结构里加一个唯一索引(business_date, channel_id)插入数据时用INSERT INTO ... ON DUPLICATE KEY UPDATE重复就执行更新而不是再次插入或者先执行一条 DELETE 删除业务日期为昨天的数据再执行 INSERT。这样无论任务失败重跑多少次最终表里同一业务日期、同一渠道只有一条数据。这个设计做完之后我再也不怕失败重试了。4.3 配置完整的调度参数在调度中心配置任务的完整参数时我是这么填的参数项配置值说明Cron0 10 2 * * ?每天凌晨 2 点 10 分执行运行模式Bean使用 XxlJob 注解类JobHandlerdailyReportJobHandler与代码中的 handler 名称一致路由策略轮询单执行器节点下效果等同“第一个”阻塞处理策略单机串行防止任务堆积、交叉执行失败重试次数3失败后间隔重试超时时间600超过 600 秒未完成视为超时特别说一下 Cron 表达式。XXL-JOB 使用的是 Quartz 的 Cron 表达式跟 Linux 的 crontab 略有差异。比如一个常见的坑Quartz 的 Cron 有“秒”这一位而 Linux crontab 没有。我第一次配置时报了 Cron 表达式错误就是因为把 Linux 的写法直接搬了过来。正确的每日凌晨两点十分执行应该是0 10 2 * * ?中间那个问号表示“不指定具体的那一天”。4.4 任务业务代码的完整结构下面这段代码大致展现了我这个报表任务的处理流程很多细节我省略了但整体骨架是可以参考的Component public class ReportDailyJob { Resource private ReportService reportService; XxlJob(dailyReportJobHandler) public void dailyReportJobHandler() throws Exception { // 1. 获取业务日期默认跑昨天的数据 String businessDate LocalDate.now().minusDays(1).toString(); // 2. 统计各渠道数据写入中间表 reportService.cleanAndReloadChannelData(businessDate); // 3. 汇总中间表写入宽表 reportService.mergeToReportTable(businessDate); // 4. 触发下游同步 reportService.notifyDownStream(businessDate); } }我在每个步骤之间都加了日志输出这样在调度中心的日志页面就能看到任务进行到哪一步了。否则你在 SQL 里跑了一个小时中途挂了连失败在哪一步都不知道排查成本极高。这里我再强调一点任务代码里不要加Thread.sleep这种“硬等”逻辑。以前 cron 脚本里 sleep 是常见操作但放到 XXL-JOB 里任务执行时间会被记录、被监控sleep 会导致日志看着是“运行中”实际上啥也没干。有依赖等需求的话应该拆成多个子任务用 XXL-JOB 的“任务依赖”功能去编排。5. 实操过程中我踩过的几个坑讲一个项目如果只讲思路不讲坑那是纸上谈兵。我把自己在实际操作中遇到的问题整理成了一份速查表每一个都标明现象、原因和解决方案希望对大家有帮助。5.1 执行器一直显示离线现象调度中心执行器管理页面执行器状态永远显示为“离线”手动触发任务会报“执行器为空或不存在”。原因排查我按优先级做了三件事先看执行器项目的启动日志确认有没有报注册失败相关的异常再确认执行器配置的admin.addresses是否写成了http://localhost:8080如果调度中心和执行器不在同一台机器localhost 是访问不通的最后确认执行器配置的port是否被防火墙拦截。我的情况是第三种测试环境的服务器防火墙没有放行 9999 端口执行器能注册到调度中心但调度中心执行调度指令时根本无法连接。放行端口后就正常了。5.2 任务连续执行多次现象凌晨报表跑完之后业务方反馈数据被覆盖了多次出现了脏数据。原因我没有考虑到调度中心本身的高可用部署。当时测试环境调度中心起了两个实例任务默认开启了故障转移策略导致两个调度中心同时把任务下发给了同一个执行器节点代码里又没有完善的幂等机制。解决办法分两层配置层面路由策略从故障转移改成“轮询”代码层面去掉重复执行的影响方案就是我前面说的“以业务日期为维度的增量覆盖”。从这次之后我养成了一个习惯所有定时任务的代码一律先考虑重复执行时是否安全再考虑功能是否实现。5.3 调度中心日志界面打不开现象任务能执行但调度中心的日志页面一直转圈看不到日志详情。原因调度中心读取日志时会去执行器部署的那台机器上拉取日志文件。如果执行器的logPath配置的目录不存在或者执行器端口访问受限日志加载就会失败。我在配置logPath时犯了一个低级错误写了一个不存在的绝对路径。后来老老实实改成/data/logs/xxl-job并提前创建好目录一切正常。5.4 重试导致的重复数据这个是报表类任务的常见问题。我虽然前面设计了唯一索引做幂等但实际比对发现中间表在重试时会被重复清理和写入导致关键时间点上的数据短暂不一致。后来我把策略改了中间表也采用“全量重刷覆盖写”的方式中间表和最终宽表都带唯一键。每次任务开始先删除业务日期的旧数据再插入新数据。这个方案配合 XXL-JOB 的“单机串行”阻塞策略目前没有出过问题。5.5 调度时间与业务时间不一致现象任务执行时间总是在整点后几秒钟才触发比配置的 Cron 晚了几秒到十几秒。原因调度中心有一个调度线程池任务多了以后线程池排队会导致触发时间有轻微延迟。这种情况通常不影响业务因为我们任务执行的是“昨天”的数据晚几秒钟没有任何影响。但如果你的任务对执行时间要求非常精确比如整点抽奖、整点发券建议把 Cron 往前调 10 秒到 15 秒比如0 10 0 * * ?改成0 45 23 * * ?之类提前一点点触发实际效果就差不了多少了。6. 从 demo 到生产任务治理和运维经验demo 跑通只是第一步。真正用起来之后你会遇到各种“看起来没毛病实际上难维护”的细节。我分享几个让我受益比较大的运维习惯。6.1 任务命名规范和组划分任务一多名字五花八门调度中心里根本分不清谁是谁。我的规则是这样的任务名称格式统一为“业务模块_动作_时间维度”例如“每日报表_经营汇总_日报”执行器 AppName 按业务域划分报表相关的都叫report-executor数仓同步的都叫dw-sync-executor一个职责单一的执行器里任务数量控制在 10 个以内太多就考虑拆分成多个执行器。这样做的好处是只要看任务列表就知道是哪个业务、干什么、跑多频繁不需要点进去看详情。6.2 日志保留与清理XXL-JOB 默认会保留每个任务的日志长时间运行后数据库和磁盘都会膨胀。调度中心的日志表xxl_job_log建议配置定期清理策略。我是通过一个额外的定时任务每天晚上 3 点删除 30 天之前的调度日志和日志文件。清理逻辑很简单核心就两条 SQLDELETE FROM xxl_job_log WHERE trigger_time DATE_SUB(NOW(), INTERVAL 30 DAY);文件清理就直接删掉日志目录下 30 天以前的子目录可以写个简单的 Shell 脚本放到 crontab 里。注意不要把日志清理和业务任务混在同一个执行器里。我当时想省事把清理任务写在同一个项目里结果清理任务失败导致日志堆积差点把磁盘打爆。6.3 监控与告警XXL-JOB 自带失败告警功能配置好邮件接收人后任务失败会自动发邮件。但提醒一下这个告警只会在任务失败时发如果任务压根没被触发比如调度中心挂了你收不到任何消息。我的做法是在业务侧加了一个“心跳校验”每天凌晨报表任务跑完后往一张监控表里写一条完成记录第二天上午 9 点用一个独立的检查任务去扫描这张表发现昨天没有完成记录就发企微通知。这样即使调度中心整个挂了我也能从业务数据层面感知到异常。6.4 任务下线与灰度上线改任务是早晚的事但千万别直接在生产环境改配置。我的流程是先在测试环境新增任务注册到测试执行器跑通验证然后把生产执行器的任务暂时置为“停止”更新代码、部署再重启任务。这里面最容易忽略的是“Glue 模式”任务。XXL-JOB 支持在控制台直接编辑代码生成任务虽然方便但代码散布在数据库里脱离了 Git 管理后续排查问题非常痛苦。我现在的策略是全部任务都用 Bean 模式代码跟随项目走版本管理。除非遇到特别紧急的临时数据处理否则不用 Glue。7. 后续可以怎么扩展这个 demo如果你觉得上面的 demo 已经跑通了想更进一步我有几个方向建议你试试7.1 结合分片广播做数据分批我们的订单明细表数据量很大单机扫描全表跑汇总比较慢。可以考虑用分片广播策略比如配置两个执行器节点每个节点各处理一半的数据比如按照订单号取模最后再合并结果。XXL-JOB 在触发分片广播任务时会把当前分片序号和总分片数传给你。代码里可以通过XxlJobHelper.getShardIndex()和XxlJobHelper.getShardTotal()拿这两个参数据此拆分数据范围。7.2 引入告警平台对接XXL-JOB 的默认告警只有邮件对国内团队来说不太友好。我见过有团队通过修改 admin 源码的方式接入钉钉/企业微信机器人但这样维护成本高。更简洁的做法是在任务代码里自己捕获异常调用统一告警平台的 HTTP 接口通知。7.3 动态调整执行时间业务方偶尔会有“今天报表提前出一个”的需求。你不用改 Cron直接在调度中心页面上点“执行一次”就能手动触发。如果你需要更加灵活的时间策略可以把 Cron 配置放到配置中心用 XXL-JOB 提供的修改接口去动态更新。我在实际使用中发现最舒服的工作状态是任务跑完自动把报表数据推送到企业微信群业务方每天早上打开手机就能看到结果完全不需要来问我“今天报表出来没”。这个体验上的提升比任何技术上的优化都更能体现调度的价值。我个人最深的体会是调度框架解决的是“什么时候跑、跑完通知谁、失败怎么办”的问题而真正决定报表质量好坏的还是你业务逻辑里对数据口径、幂等处理、异常边界这些细节的把握。不要因为上了 XXL-JOB 就觉得万事大吉它只是把“定时”这件事管好了剩下的每一步都要靠你自己在代码里守住。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。