如果你也经历过以下场集群部署后任务重复执行数据错乱某个节点挂了任务没人接管业务停滞任务执行时间越来越长想拆分并行处理却无从下手老板问昨晚那个定时任务跑了没你只能去服务器上翻日志分布式定时任务框架就是解决这些问题的标准答案。今天我们把国内最主流的三个框架——XXL-JOB、Elastic-Job、PowerJob——放在一起说。一、三个框架一张图看懂定位维度XXL-JOBElastic-JobPowerJob出身大众点评开源当当开源阿里云前员工开源核心依赖自研轻量调度ZooKeeper自研无外部依赖任务分片支持执行器分片核心特性分片弹性扩容支持MapReduce模式故障转移支持失败重试告警支持弹性分片转移支持任务实例级重试动态调度控制台手动触发/CRON支持支持时间表达式API触发延迟任务不支持不支持支持秒级延迟队列监控告警内置邮件/短信/WebHook需自行集成内置较完善学习曲线低30分钟上手中需理解ZK分片概念中社区活跃度⭐⭐⭐⭐⭐极高⭐⭐⭐维护放缓⭐⭐⭐⭐选择建议如果团队追求快速落地、文档完善、社区活跃→XXL-JOB国内80%的中小厂选这个如果业务有海量分片需求如千万级数据批量处理→ Elastic-Job但注意社区维护问题如果需要延迟任务 复杂工作流编排→ PowerJob功能最全但生态相对年轻我见过的生产环境10个里有8个用的是 XXL-JOB。不是因为它最强而是它够好用、够简单、出了问题能搜到答案。技术选型不是选最强的是选最适合你团队当下阶段的。二、XXL-JOB 架构解剖XXL-JOB 的架构非常干净只有两大角色核心设计要点调度中心与执行器分离调度中心只负责什么时候发指令执行器只负责干完活回报结果数据库行锁实现分布式调度利用 MySQL 的for update悲观锁确保同一时刻只有一个调度中心节点在触发任务执行器自动注册执行器启动后主动上报 IP 和端口调度中心维护存活节点列表失败重试 转移任务失败时调度中心可以将任务路由到另一台执行器重试三、实战代码从零搭建 XXL-JOB3.1 执行器配置Spring Boot 3 版本dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.0/version /dependencyConfiguration public class XxlJobConfig { Value(${xxl.job.admin.addresses}) private String adminAddresses; Value(${xxl.job.executor.appname}) private String appName; Value(${xxl.job.executor.port}) private int port; Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); // 调度中心地址 executor.setAppname(appName); // 执行器名称注册到调度中心 executor.setPort(port); // 执行器监听端口用于接收调度指令 executor.setLogRetentionDays(30); // 日志保留30天 // 注意生产环境建议配置 accessToken 做安全校验 // executor.setAccessToken(your-secret-token); return executor; } }# application.yml xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin executor: appname: my-business-executor port: 9999注意这个设计执行器自己开一个 HTTP 端口默认9999调度中心通过 HTTP 请求把任务派发过来。这意味着执行器可以被调度中心主动找到而不是像某些框架那样执行器去轮询拉任务。主动推送的延迟更低。3.2 定义一个分片任务海量数据处理场景假设你要给全量用户发优惠券单机跑要 2 小时想拆成 4 台机器并行跑Component public class CouponDispatchJob { XxlJob(dispatchCouponSharding) public void execute() { // XxlJobHelper 提供当前分片上下文 int shardIndex XxlJobHelper.getShardIndex(); // 当前分片序号0, 1, 2, 3 int shardTotal XxlJobHelper.getShardTotal(); // 总分片数4 // 模拟从数据库捞取该分片负责的用户 // SQL: SELECT * FROM user WHERE MOD(id, 4) #{shardIndex} ListUser users userMapper.selectByShard(shardIndex, shardTotal); XxlJobHelper.log(分片[{}/{}] 开始处理 {} 个用户, shardIndex, shardTotal, users.size()); int successCount 0; for (User user : users) { try { couponService.sendTo(user); successCount; } catch (Exception e) { // 单条失败不影响整体记录日志继续 XxlJobHelper.log(用户 {} 发券失败: {}, user.getId(), e.getMessage()); } } // 设置任务结果调度中心可见 XxlJobHelper.handleSuccess(分片 shardIndex 完成成功/总量: successCount / users.size()); } }分片的核心逻辑调度中心把分片总数和当前分片序号通过 HTTP 请求传给每个执行器你的业务代码根据shardIndex决定处理哪一部分数据常用分片策略MOD(id, shardTotal) shardIndex或按时间区间、按地区划分分片任务让我最爽的一次是把一个 3 小时的账单结算任务拆到 8 台机器上20 分钟跑完。老板以为我优化了什么算法其实我只是加了两行分片代码。3.3 自定义任务路由策略二次开发示例XXL-JOB 默认的路由策略有轮询、随机、一致性哈希、最近最久未使用等。但业务中常有这样的需求这个报表任务必须跑在有大数据集群 VPN 的那台机器上这时候就需要自定义路由策略。XXL-JOB 的执行器支持通过xxl.job.executor.address手动指定IP但更优雅的方式是扩展路由策略/** * 自定义路由策略按机器标签路由 * 适用于特定任务必须跑在特定配置的机器上如带GPU、带内网VPN等 */ Component public class TagBasedRouter implements ExecutorRouter { Override public ReturnTString route(TriggerParam triggerParam, ListString addressList) { // 从任务参数中读取目标标签如 tagGPU String targetTag triggerParam.getExecutorParams(); for (String address : addressList) { // 实际生产中标签可以从配置中心或执行器上报的元数据中获取 // 这里简化演示假设 IP 段 192.168.1.x 是 GPU 机器 if (targetTag ! null targetTag.contains(GPU) address.startsWith(192.168.1)) { return new ReturnT(address); } } // 匹配不到fallback 到第一个可用节点 return new ReturnT(addressList.get(0)); } }然后在调度中心配置任务时在路由策略中选择自定义策略并在任务参数中传入GPU即可。二次开发的小建议XXL-JOB 的源码结构很清晰com.xxl.job.admin.core.route包下是所有路由策略com.xxl.job.admin.core.trigger是触发逻辑。如果你团队有特殊的调度需求如按业务优先级排队、按资源负载动态选择节点直接在这两个包下扩展即可。我改过两次每次半天搞定。四、故障转移与动态调度原理4.1 故障转移XXL-JOB 的故障转移发生在两个层面调度层面调度中心触发任务时如果目标执行器心跳超时默认 30 秒无响应会自动从存活列表中剔除并把任务路由到其他节点。执行层面任务在执行器上跑的时候如果抛异常调度中心会根据配置的失败重试次数重新触发。注意这里的重试是重新调度一次完整任务不是断点续传。重要提醒XXL-JOB 不提供任务的幂等性保证。如果你的任务本身不幂等比如发优惠券、扣库存一定要在业务层做好防重。常见做法数据库唯一索引Redis 分布式锁SETNX job_name_20241111 true EX 3600任务开始前先查状态表已处理的直接跳过4.2 动态调度XXL-JOB 的控制台支持手动触发点一下按钮立即执行常用于补数据CRON 表达式标准的 Quartz CRON支持到秒级任务依赖A 任务执行成功后自动触发 B 任务DAG 工作流调度类型切换可以在无、CRON、固定速度之间随时切换无需重启控制台截图-worthy 的功能执行日志实时查看。任务跑完后不用 SSH 上服务器tail -f直接在页面上看控制台输出还能下载完整日志。这个对排查生产问题太友好了。五、三个框架的详细选型决策树六、建议建议一执行器务必配置 accessToken默认情况下 XXL-JOB 执行器的 HTTP 端口是裸奔的。如果部署在公网环境或者内部网络被渗透攻击者可以直接向你的执行器端口发送任务触发请求。// 调度中心和执行器都要配 executor.setAccessToken(your-32-char-random-string);建议二任务日志要独立别和业务日志混在一起XXL-JOB 的XxlJobHelper.log()会把日志写入独立的文件通过控制台可以直接查看。千万别在任务里只用log.info()生产出问题你得一台台服务器去查。建议三给关键任务加上超时时间和失败告警在调度中心配置任务时设置任务超时时间比如 30 分钟防止任务卡死一直占着资源配置失败重试次数一般 3 次开启失败告警绑定企业微信/钉钉 WebHook我见过最惨的事故一个数据同步任务因为网络抖动卡住没有超时设置也没有告警停了 6 个小时才发现下游数据全部滞后。单机定时任务是玩具分布式定时任务是工程。选 XXL-JOB 不会错但别忘了在业务层做好幂等和防重。下篇预告Day 53 | Docker从入门到容器化部署Spring Boot应用——我们把这 52 天写的所有服务打包成镜像从能跑进化到哪里都能跑。本文为「Java后端工程师进阶之路」专栏第52篇,完整大纲见Java全栈工程师系统培养大纲。