ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

XXL-JOB实战:从定时任务痛点出发,快速搭建分布式调度平台

XXL-JOB实战:从定时任务痛点出发,快速搭建分布式调度平台 1. 先说清楚为什么我不建议你再手动跑定时任务做后端开发的人十有八九都经历过这种场景数据库里有一批业务数据需要每天凌晨定时同步报表要每半小时刷新一次缓存要定期清理或者凌晨要给一批用户推送通知。早期的项目里这些需求最常见的实现方式就是Spring自带的Scheduled注解或者直接在服务器上挂一个Cron脚本更原始一点的干脆靠人定闹钟手动跑。Scheduled注解确实简单写个方法加个注解就完事。但随着项目拆分、服务增多问题就来了定时任务散落在各个服务里没有统一的管理界面改一次执行时间要重新部署任务挂了你不知道日志还得去服务器上翻。最头疼的是如果你做了多实例部署同一个任务在每台机器上都会执行一遍数据同步这种任务一旦重复执行结果就是数据错乱。我见过不止一个项目因为这种重复执行把库存表刷出负数的。xxl-job解决的就是这一整类问题。它是一个轻量级的分布式任务调度平台核心思路是调度逻辑和执行逻辑分离调度中心负责管理所有任务、配置执行时间、触发执行、记录日志执行器是嵌在业务项目里的一个模块负责接活儿、干活、上报结果。中间通过网络通信完成任务的派发天然支持多实例、集群环境同一个任务可以指定由哪台机器执行也可以用轮询、分片广播这类策略路由到多个执行器上。这篇教程我按实操来写目标是让你在5分钟内把xxl-job跑起来并且跑通调度中心配任务业务项目执行任务的完整链路。我会用Docker部署调度中心用Spring Boot写一个执行器示例任务场景选的是最常见的数据更新同步——这也是xxl-job用得最多的场景。适合刚接触分布式任务调度、想在项目里快速落地的同学也适合已经把Scheduled写出痛点、想迁移到统一调度平台的团队参考。2. 环境准备用Docker部署xxl-job调度中心很多教程让你先去官网下载源码、自己打包、改配置文件再跑起来这步其实劝退了很多人。xxl-job提供了现成的Docker镜像只要你的机器上有Docker调度中心5分钟就能起好。2.1 先准备好MySQL和初始表调度中心本身不存业务数据但它需要保存任务配置、调度日志、执行器注册信息这些元数据所以必须有一份数据库。这里有个关键点xxl-job的调度中心不会自动建表你必须手动执行官方提供的SQL脚本。我用的版本是2.3.0这个版本非常稳定网上资料也最多。别一上来就追最新版除非你有特殊需求。对应版本的SQL脚本可以从Gitee上找到文件名叫tables_xxl_job.sql在/doc/db/目录下。你先在MySQL里建一个数据库然后把这个脚本导进去mysql -u root -p -e CREATE DATABASE xxl_job DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p xxl_job tables_xxl_job.sql导入成功后至少能看到下面这些表表名用途xxl_job_info任务配置信息xxl_job_log调度日志xxl_job_registry执行器注册信息xxl_job_group执行器分组xxl_job_user登录用户如果导入时报错了90%是SQL文件编码问题用utf8mb4重建数据库再试一次。还有一点要注意MySQL版本建议5.7或8.0太老的版本字符集支持不完善太新的版本某些驱动不兼容也会有奇怪的报错。2.2 拉镜像并启动调度中心建好库之后调度中心的启动就简单了。我用的是Docker官方镜像xuxueli/xxl-job-admin版本标签跟项目版本保持一致所以这里拉2.3.0docker run \ -p 8080:8080 \ -e PARAMS--spring.datasource.urljdbc:mysql://127.0.0.1:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai --spring.datasource.usernameroot --spring.datasource.password123456 \ -v /data/xxl-job/applogs:/data/applogs \ --name xxl-job-admin \ -d xuxueli/xxl-job-admin:2.3.0这段命令我要拆开讲一下很多人就是在这里踩坑的。首先是-e PARAMS。这个镜像里的启动脚本会把PARAMS的内容拼到Java启动命令后面所以你可以在这里覆盖Spring Boot的配置。数据库连接串里的127.0.0.1指的不是容器内部的地址而是宿主机的地址。如果你跟我一样是MySQL跑在宿主机上、Docker跑在同一个机器上这么写没问题。但如果你是Mac或者Windows上用Docker Desktop容器访问宿主机要用host.docker.internal这个特殊域名写成jdbc:mysql://host.docker.internal:3306/xxl_job。然后是-v /data/xxl-job/applogs:/data/applogs。这行把容器里的日志目录挂载到宿主机上方便你直接查看调度中心自己的运行日志。不加也能启动但出了问题排查起来麻烦建议加上。启动之后用docker logs -f xxl-job-admin盯一下日志看到类似这样的输出就说明启动成功了Started XxlJobAdminApplication in 3.87 seconds然后浏览器访问http://localhost:8080/xxl-job-admin默认账号密码是admin/123456登录进去就能看到调度中心的管理界面。2.3 登录后要做的三件事调度中心能登录只是第一步我建议你先花一分钟做三件事能省下后面一堆麻烦。第一件事是改密码。默认账号密码谁都知道如果你的调度中心端口暴露在外网就等着别人来帮你执行任务吧。虽然大多数场景下调度中心只在内网跑但养成习惯没坏处。第二件事是确认系统管理-执行器管理页面是全空的。新装好的调度中心还没有任何执行器后面你会看到自己注册上来的。第三件事是回到docker run那句命令确认你设置的accessToken。默认情况下xxl-job的执行器和调度中心之间是不做鉴权的只要对方知道地址就能注册。如果你生产环境要走公网或者公司有安全要求可以在启动命令里加上--xxl.job.accessToken你的自定义Token然后后面Spring Boot集成时配同一个值。不改也行本地开发完全够用。3. Spring Boot项目集成执行器调度中心只是大脑真正的活儿得由嵌入在业务服务里的执行器来干。这一步我带你从头建一个Spring Boot项目一步步把它变成xxl-job的执行器。3.1 引入xxl-job-core依赖并配置参数先在你的pom.xml里加入xxl-job的核心依赖dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.3.0/version /dependency然后往application.yml里加配置。为了让配置项清晰我惯用的写法是这样xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin accessToken: default_token executor: appname: xxx-data-sync address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30逐个解释一下这些配置的作用这部分是理解执行器工作原理的关键admin.addresses调度中心的访问地址。如果有多个调度中心做集群用逗号分隔写多个地址。accessToken执行器和调度中心通信的凭证两边必须一致。executor.appname执行器的应用名也叫AppName。这个非常重要后面在调度中心注册执行器时靠的就是这个名字。executor.port执行器自己暴露的HTTP端口调度中心通过这个端口把任务JobHandler的请求发过来。注意别跟项目里其他端口冲突。executor.ip默认留空就行执行器会自动探测本机IP注册到调度中心。只有当自动探测出错时才需要手动指定。executor.logpath任务执行日志的存放路径调度中心界面上查到的日志其实是从这个路径读出来的。executor.logretentiondays任务日志保留天数避免日志无限堆积。3.2 编写执行器配置类配置类很简单就是把上面的参数组装成一个XxlJobSpringExecutor的Bean交给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 executor new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); executor.setAppname(appname); executor.setAddress(address); executor.setIp(ip); executor.setPort(port); executor.setAccessToken(accessToken); executor.setLogPath(logPath); executor.setLogRetentionDays(logRetentionDays); return executor; } }这段代码基本是固定的核心逻辑就一句话XxlJobSpringExecutor启动时会自动向调度中心注册当前执行器然后把项目里所有标注了XxlJob注解的方法扫描进本地的映射表。调度中心下发任务时它根据任务配置的JobHandler名称在本地找到对应的方法并反射调用。3.3 用XxlJob注解写一个数据同步任务既然示例场景是数据更新同步我就写一个实际工作中最常见的任务从外部接口或者临时表拉取变更数据更新到业务表里。为了演示方便我用一个简单的打印逻辑代替真实业务你只需要关注XxlJob注解的用法Component public class DataSyncJob { private static final Logger logger LoggerFactory.getLogger(DataSyncJob.class); XxlJob(syncDataJobHandler) public void syncDataJobHandler() throws Exception { XxlJobHelper.log(数据更新同步任务开始执行...); // 这里写你真实的同步逻辑 // 比如从a表查出增量数据写入b表 ListLong ids queryChangedIds(); int count 0; for (Long id : ids) { boolean success doSync(id); if (success) { count; } } XxlJobHelper.log(本次同步完成共同步 {} 条数据, count); // 如果同步结果不满足预期可以通过 XxlJobHelper 返回执行失败 // if (count ! expectCount) { // XxlJobHelper.handleFail(同步数量不一致); // return; // } XxlJobHelper.handleSuccess(同步成功); } }这里有几个细节值得说道说道。第一XxlJob注解里的字符串就是JobHandler名称这个名字要全局唯一就是你给这个任务起的方法名。后面在调度中心创建任务时要靠这个名字来指定执行哪个JobHandler。第二尽量用XxlJobHelper.log()打印日志而不是直接用logger.info()。原因是XxlJobHelper.log()的日志会跟随当前任务存入调度中心你在管理界面能看到。而logger.info()只写到本地文件调度中心看不到。第三任务的执行结果用XxlJobHelper.handleSuccess()和XxlJobHelper.handleFail()来标记。如果你不显示调用这些方法任务默认也算成功。这就是很多任务显示成功但实际上没干活的坑的来源所以关键业务我建议显式捕获异常并调用handleFail()。写完这些启动你的Spring Boot项目。注意观察控制台日志正常的话你会看到执行器启动并注册成功的标识。下面这样就说明执行器已经连上调度中心了xxl-job register executor success, appnamexxx-data-sync, addresshttp://192.168.1.100:9999/4. 在调度中心配任务跑通全流程执行器起来了调度中心也起来了现在把它们串起来。这一步我在调度中心的界面上操作你跟着点就行。4.1 注册执行器回到调度中心管理界面打开执行器管理页签点击新增执行器按钮。AppName填xxx-data-sync必须和application.yml里配置的xxl.job.executor.appname完全一致。名称填一个能让人看懂的名字比如数据同步服务。注册方式选自动注册。你直接在Spring Boot项目里配置好执行器它会自动出现在这个列表里。手动注册的方式适用于执行器部署在非标准网络环境、自动注册失败的情况一般用不上。保存之后如果看到执行器列表里出现了一条记录且OnLine 机器地址列显示了你本机的IP和9999端口恭喜执行器注册成功。4.2 创建调度任务打开任务管理页签默认会连接到当前执行器分组下的任务列表但列表肯定是空的。点击新增任务这里要注意几个关键字段执行器选择刚才创建的数据同步服务。任务描述写清楚这个任务是干嘛的比如每5分钟同步一次xx表更新数据。这个描述是给团队看的一定要写清楚否则半年后没人记得这个任务是干什么的。调度类型选Cron。Cron表达式这里是重头戏。拿每5分钟执行一次来说表达式是0 0/5 * * * ?。注意Cron表达式有6位或7位之分xxl-job用的是7位秒 分 时 日 月 周 年很多从Quartz转过来的同学容易在第1位丢掉秒。比如你想每天凌晨2点执行不能只写0 2 * * *要写0 0 2 * * ?否则任务会在凌晨2点的第一秒才执行吗不是那样写的话秒位会被当成第二个字段整体语义就错了任务根本不会按预期触发。运行模式选BeanJobHandler填syncDataJobHandler必须和代码里XxlJob(syncDataJobHandler)的名称一致。注意这里不是写方法名是注解里配的字符串。路由策略默认第一个就行。这个策略决定了任务分发到哪台执行器上执行。你的执行器只有一台选什么都一样。等以后服务扩容了你可以选轮询做负载均衡或者选分片广播让每台机器各处理一部分数据。阻塞处理策略默认单机串行。这个参数用来处理上一次任务还没跑完下一次触发时间又到了的情况。选单机串行就是排队等着选丢弃后续调度就是新的触发直接跳过。对于数据同步这类不能并发的任务推荐单机串行。任务超时时间设置一个超时值比如300秒超过这个时间任务会被标记为超时。如果不设置任务卡死你都不知道。保存任务后你会看到任务列表里多了一条记录默认是停止状态。点一下启动任务就开始按照Cron表达式触发。4.3 手动执行、查看调度结果和日志任务刚配好你可能不想等一个Cron周期没关系调度中心支持手动触发。在任务列表的操作栏里点击执行一次按钮任务会被立即下发到执行器上执行。执行完之后点击日志按钮进入调度日志页面。这里能看到每次调度的详细记录调度时间调度中心下发任务的时间。调度结果调度中心成功把任务下发出去会显示成功。执行结果执行器实际执行的结果。可以看到执行成功或执行失败。执行日志点进去能逐行看到你用XxlJobHelper.log()输出的内容。这一页是排查问题的主战场比业务项目自己的日志还要好用因为它把调度信息、执行时机、处理结果全部关联在了一条时间线上。我强烈建议你点一次执行一次按钮然后去日志页看一遍完整流程。看完之后你就理解了xxl-job的整体机制调度中心并不执行你的任务代码它只负责到了时间就叫一嗓子真正干活的是执行器。5. 我踩过的坑常见排查大全这部分我梳理了自己实际使用中遇到过的、以及帮别人排查过的典型问题按排查思路整理出来遇到问题直接对照着查。5.1 调度中心起不来数据库连接和时区问题新手最常见的就是调度中心启动失败。打开docker logs xxl-job-admin看到的报错五花八门但根因基本集中在两类一类是数据库连接失败。Communications link failure或者Access denied for user。前者是因为连接串里的数据库地址写错了记住容器内不能直接用localhost访问宿主机要区分环境和系统选用127.0.0.1还是host.docker.internal。后者就是账号密码不对去检查--spring.datasource.username和--spring.datasource.password。另一类是时区问题。日志里出现The server time zone value ... is unrecognized这是数据库时区没配对调度中心启动时访问xxl_job表被拒。解决方式是在JDBC连接串后面加serverTimezoneAsia/Shanghai像我在2.2节里写的那样。如果你用的是MySQL 5.7还可能会遇到Unknown initial character set index 255这是客户端连接时用了不支持的字符集编码把连接串改为characterEncodingUTF-8基本都能解决。5.2 执行器注册不上网络、AppName、token三大原因项目能正常启动但调度中心的执行器列表里看不到机器地址。按下面顺序排查命中率几乎100%看日志。执行器启动时如果有xxl-job register executor fail这样的日志说明连不上调度中心。确认admin.addresses的地址和端口是否正确。核对AppName。调度中心新增执行器时填的AppName和项目里executor.appname必须完全一致多一个字符、少一个字符都注册不上。注意大小写也敏感。检查网络。如果执行器和调度中心不在同一台机器确认防火墙有没有放行三个端口调度中心的8080端口、执行器的9999端口。我在公司帮别人排查时发现过无数次Windows防火墙默认把Java程序的入站连接拦了。telnet 执行器IP 9999通不通一测便知。检查token。如果你的调度中心设置了accessToken项目里没配或者配的值不一样注册请求会被拒绝。这个报错在日志里不会特别明显但调度中心日志会有401相关记录。5.3 任务不触发或重复触发Cron表达式和路由策略任务创建之后一直不跑第一反应去看日志页。如果调度日志里面压根没有记录说明调度中心就没触发过问题在Cron配置。这里最容易犯的错我前面提过了xxl-job的Cron是7位第1位是秒。0 0 2 * * ?是每天凌晨2点0 0/5 * * * ?是每5分钟0 0 * * * ?是每小时整点。每次新建任务先在界面右上角找到生成Cron的组件它会实时解析下一次执行时间确认无误再保存。另一种情况是调度日志有记录但显示调度失败或执行失败。调度失败通常是执行器失联回到5.2检查。执行失败则去点开执行日志看业务代码里报了什么错。这里有个排查顺序的诀窍先看调度日志的执行结果列再看执行日志里的具体堆栈。前者告诉你任务有没有到执行器后者告诉你业务代码死在哪一步。坑还容易出现在重复触发。比如你配了0 0 2 * * ?每天凌晨2点跑同步但执行器做了集群部署默认路由策略是第一个只有一台机器执行没问题。但如果你用了分片广播它会把一个任务同时发给所有执行器跑一遍——很多人的重复执行问题就是这么来的。数据同步、数据清洗这类任务默认用第一个或轮询策略即可单据生成、报表统计同理。5.4 Docker部署的执行器IP自动注册错误这是Docker化部署以后特有的坑。执行器默认自动探测本机IP但如果你在容器里跑Spring Boot探测到的往往是容器内部IP比如172.17.0.x而不是宿主机的对外IP。调度中心拿到这个IP后去连执行器完全连不通。解决办法有两种。一种是在application.yml里手动指定xxl.job.executor.ip填上宿主机对外可达的IP。适用于IP固定的场景一了百了。另一种是用host网络模式启动容器和宿主机共享网络栈自动探测就能拿到宿主机的真实IPdocker run --network host --name executor-service your-image:tag这种方式简单直接但缺点是端口管理不像-p那么灵活适合不想改配置的场景。我的建议是开发环境随便生产环境要么手动指定IP要么用一个稳定的内网域名。6. 几个值得长期注意的设计经验把整个链路跑通之后最后再分享几个我长期使用中沉淀下来的体会。版本匹配是第一优先级。xxl-job调度中心和执行器通过HTTP协议通信不同大版本之间协议有些微差异保险的做法是保持调度中心镜像版本、xxl-job-core依赖版本完全一致。我见过把2.3.0的调度中心配2.4.0执行器的注册功能正常但任务下发时偶发反序列化异常排查了半天最后发现是版本问题。所以能跑就尽量别升级这句话在xxl-job这里尤其适用。第二是日志保留策略要提前想。我的一个线上服务每次数据同步会打印大量明细日志默认保留30天几个月下来磁盘就满了。设置logretentiondays时要结合任务频率和日志量来评估宁可取小别取大调度历史在数据库里还有一份不太依赖执行器本地的日志文件。第三是关于执行器进程的优雅退出。Spring Boot应用关闭时要确保XxlJobSpringExecutor能先把执行器从调度中心摘除掉否则调度中心会在一段时间内还认为这台机器在线把任务发到一个已经挂掉的进程上。正常走Spring的PreDestroy机制没有问题但如果你用kill -9强杀进程就只能等待调度中心的过期清理了。我个人的习惯是每次新增定时任务都先手动执行一次验证逻辑确认正常后再把Cron配上去启动。这个小动作看起来多花了一分钟但它能帮你把任务上线后发现逻辑有误的概率降到最低。定时任务的坑往往不在配置上而在业务逻辑对执行时机的假设上——你觉得数据应该已经准备好了结果上游还没跑完。这一类问题只有长期观察才能摸清规律。市面上比xxl-job更重的调度平台还有不少但如果你只是想要一个能统一管理、支持集群、日志清晰、部署简单的方案xxl-job确实是这个体量下的一个很舒服的选择。希望这篇文章能帮你少走点弯路至少把调度中心起不来和执行器注册不上这两大拦路虎提前排掉。
返回列表