最近在学任务调度。起因是我整理技术资料的时候,发现 Quartz 和 XXL-Job 总是被放在一起对比,几乎每篇都在说"XXL-Job 比 Quartz 好用",但具体好在哪里,没人细讲。我工作快1年,平时主要写业务代码,定时任务只在项目里见过别人用 @Scheduled,自己从没正经接触过。所以想趁这次把这两个东西搞清楚,顺手记一份笔记。

先说清楚:这篇笔记里没有"实际部署过"的经验,都是我这两周看文档、看别人踩坑记录整理出来的。代码也是从资料里抄下来理解的,我自己没跑过。写到哪算哪,有说错的地方欢迎指正。

我的起点:定时任务 = 闹钟

我对定时任务最初的认知就是 Java 的 TimerScheduledExecutor。Timer 太老了,项目里一般都用 ScheduledExecutorService,我后来查了文档确认,基本用法大概是:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.HOURS); // 每小时执行一次

单机的时候完全够用。就像手机闹钟:设个时间,到点响。系统只有一台机器,定时任务跑在上面,没人需要关心它是怎么被调度的。

单机够用,上了集群就出问题

真正让我觉得"任务调度"值得学的是这个场景:如果同样的定时任务部署在 10 台服务器上,到点之后,10 台机器会同时执行。比如每天凌晨清一次临时表,10 台机器一起清,数据重复处理倒是小事,有些任务(比如发消息)就会出大事。

我第一次意识到这个问题的时候挺惊讶的,因为我之前根本没想过定时任务会和"分布式"扯上关系。也是从这时候开始,我理解为什么会有 Quartz、XXL-Job 这些专门做任务调度的东西。

先看的 Quartz:老牌、资料多,但差点把我劝退

第一印象

Quartz 是 Java 生态里最老牌的任务调度库,资料多到看不完。第一印象是"专业、厚重",网上说它统治了 Java 调度市场十几年,这点我没什么争议。

三个核心概念,用我自己的话讲

Quartz 有三个核心概念,我理解成:

  • Scheduler:调度器,像调度室,管着所有任务,决定谁在什么时候执行
  • Trigger:触发器,定义执行时间。Cron 表达式在这里,支持得特别全,什么"每月最后一个周五的上午 10:15"都能表达
  • Job 和 JobDetail:Job 是到点之后要干的活,JobDetail 是这份活的说明书,写着任务叫什么名字、需要传什么参数

卡住我的地方:Quartz 的"集群"到底是什么意思

这是我这周看资料时第一个被误导的地方,也是我觉得最值得记录的一点。

我一开始看到"Quartz 支持集群",第一反应是:好家伙,那任务是不是能分到多台机器上并行跑了?比如有个任务要处理 100 万条数据,单机跑要 1 小时,集群里 10 台机器一摊,每台处理 10 万条,6 分钟不就能跑完了?

后来连着看了好几篇源码分析才明白,完全不是这么回事。Quartz 的集群是高可用,不是并行

它的原理是数据库行锁:集群里的每个调度器节点,要触发任务之前先去数据库抢一把锁(锁表叫 QRTZ_LOCKS,用的 SELECT FOR UPDATE),谁抢到锁谁执行。所以同一个任务在同一时刻只会有一个节点在执行。这样做的目的不是"跑得快",是"节点挂了任务不停":A 节点挂了,锁超时释放,B 节点接管,任务不会因为单点故障而中断。

代价也很明显:节点越多,抢锁越频繁,数据库压力越大。任务一多,数据库就成了瓶颈。

"支持集群"和"分布式并行"是两回事。我以前把这两个概念混在一起,这个认知转变对我挺重要的。

Quartz 的局限,我看下来比较在意的

  1. 没有管理界面。 它是库,不是平台。想改任务的 Cron?写代码调 API,或者直接改数据库。想看执行日志?去服务器上 grep,或者自己建日志表。对不写代码的运维同事来说,基本是黑盒。
  2. 集群依赖数据库,压力大。 上面说过了,抢锁和大量 SQL 查询都是开销。
  3. 任务不能拆分并行。 一个大任务想分给多台机器跑,Quartz 本身不支持,得自己想别的办法。

再看 XXL-Job:国产开源,思路完全不一样

第一印象

XXL-Job 是国产开源的分布式任务调度平台,GitHub 上 star 很高,社区活跃。最让我舒服的是文档是中文的,对英语一般的人来说太友好了。网上普遍说它"轻量、易用",看完架构之后我觉得,它和 Quartz 的思路确实完全不同。

核心架构:调度和执行分开

XXL-Job 把"调度"和"干活"拆成了两个角色:

  • 调度中心:一个独立的 Web 应用,只负责管任务、定时间、发指令,不跑业务逻辑
  • 执行器:嵌在业务应用里,收到指令后真正干活

我的理解:Quartz 是"每个节点自己调度、自己抢锁",XXL-Job 是"一个指挥官只管发号施令,士兵只管执行"。指挥官说"凌晨 2 点,任务 A 开始",所有执行器收到指令,按路由策略决定具体谁干。

通信方式上,调度中心通过 HTTP 调用执行器,执行器内置了一个基于 Netty 的 HTTP 服务来接收请求,执行完了再回调结果。

路由策略:任务让谁跑,可以自己定

这是我觉得 XXL-Job 比 Quartz 聪明的地方。Quartz 抢锁,谁抢到谁执行,你控制不了;XXL-Job 可以明确指定:

  • 第一个 / 最后一个:固定选哪台机器
  • 轮询:按顺序轮流来
  • 随机:随机选在线机器
  • 一致性 Hash:相同参数的任务永远分到同一台机器,适合缓存类任务
  • LFU:选使用频率最低的机器
  • 故障转移:检测到机器挂了,自动切到下一台健康的

让我"啊原来如此"的地方:分片广播

这是我看资料时最兴奋的部分。前面说 Quartz 不能把一个大任务拆开并行跑,XXL-Job 的分片广播就是专门干这个的。

思路是:任务触发时,调度中心通知集群里所有执行器"你们都要跑这个任务",然后每个执行器通过 XxlJobHelper 拿到自己是第几片、总共有多少片,只处理自己分到的那部分数据。

我从文档里抄下来一段示例,配合注释看才理解:

int shardIndex = XxlJobHelper.getShardIndex();   // 我是第几片,0 到 total-1
int shardTotal = XxlJobHelper.getShardingTotal(); // 一共有几片
// 只处理自己这一片的数据,比如按 id 取模
List<Data> list = db.query("select * from table where mod(id, 10) = " + shardIndex);

10 台机器,每台处理 1/10,原来 1 小时的任务理论上 6 分钟能跑完。这段代码我看了好几遍,因为它是全文唯一一段"直接解决了我之前以为 Quartz 做不到的事"的代码。

不过我只是理解了思路,没实际跑过。而且我知道分片任务得做成幂等的:节点挂了重跑,分片参数变化,可能重复处理,这块我还没想透。

其他印象比较深的点

  • GLUE 模式:在网页上直接写代码保存,不用重启应用,任务就能跑新逻辑(支持 Java、Shell、Python 等)。对快速改任务挺友好,但我没试过
  • 日志和告警:执行日志自动落库,网页上能看;任务失败可以邮件、钉钉告警。Quartz 这边全得自己造

一张表看完我的理解

维度QuartzXXL-Job
定位调度库,嵌在你的应用里调度平台,独立调度中心 + 执行器
集群原理数据库行锁抢锁,高可用不并行调度中心发指令,路由策略决定谁执行
任务并行不支持,自己想办法分片广播
管理界面没有,自己造自带 Web 界面
数据库依赖强依赖,集群下压力大弱依赖,只存元数据和日志

这张表是我看完资料自己总结的,属于纸面理解。我特别想强调第一行:Quartz 是"库",XXL-Job 是"平台",这两个词的区别基本决定了后面所有的差异。

代码写法对比(都是我从资料里整理的,没跑过)

拿同一个需求对比:每天凌晨 2 点统计用户活跃度,写入报表。

Quartz 大概要写一个 Job 类,再配一堆东西:

public class UserStatsJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        // 业务逻辑:查库、计算、写报表
        // 事务、异常、日志都要自己处理
    }
}

然后在 Spring 里配 SchedulerFactoryBeanJobDetailCronTrigger。想动态改执行时间,得注入 SchedulerrescheduleJob 方法。

XXL-Job 这边:

@Component
public class UserStatsXxlJob {
    @XxlJob("userStatsJobHandler")
    public void execute() {
        XxlJobHelper.log("开始执行统计任务");
        // 业务逻辑,可以用 XxlJobHelper.handleSuccess() / handleFail() 手动控制结果
    }
}

然后部署调度中心,业务项目引依赖、配置调度中心地址,启动后执行器自动注册。Cron 不用写在代码里,在网页上新建任务,Job Handler 填 userStatsJobHandler,Cron 填 0 0 2 * * ?,点启动就行。

我的感受:XXL-Job 把"调度配置"从代码里挪到了界面上,代码里只剩业务逻辑,这确实更符合运维的诉求。代价是要多维护一个调度中心服务。Quartz 一个库就搞定,部署简单,单体应用完全够用。

我的选型思考(纯纸面,没实测)

如果现在让我选,我倾向 XXL-Job,理由是:

  1. 我们这边是微服务架构,任务天然分布在不同服务里,XXL-Job 的路由策略和故障转移是现成的
  2. 有管理界面,运维和测试能自己看日志、手动触发,不用每次找我改代码
  3. 分片广播对数据量大的任务很实用

但我必须说,这只是看资料得出的判断,没经过实践验证。Quartz 在单体场景、复杂 Cron 需求(它支持 Calendar 排除特定日期,比如节假日)上也有自己的位置。网上很多"XXL-Job 碾压 Quartz"的说法,我现在是说不出口的,自己没验证过的东西,不好下结论。

还没搞懂的

  • 调度中心集群也是靠数据库锁做 HA,那任务量大了会不会有类似 Quartz 的数据库瓶颈?我看到的资料说法不一,没想清楚
  • 分片广播在节点挂掉重跑时怎么保证幂等,前面提过,还没想透
  • GLUE 模式的性能和实际维护体验怎么样
  • 云原生环境下 K8s 的 CronJob 和 XXL-Job 怎么选,我只知道有这回事,还没开始学

写在最后

这篇笔记从"Quartz 和 XXL-Job 谁更好"这个问题开始,学到一半发现答案没那么重要。先搞清楚"调度"和"执行"的区别、"高可用"和"并行"的区别,比急着站队有意义得多。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐