做过SpringBoot后端的人,基本都会碰到定时任务的需求。从最早在方法上加一行@Scheduled,到后来发现多个实例同时跑导致数据重复,再到引入XXL-JOB做统一调度,这条路我走过不少弯路。今天要聊的是我在一个实际项目中,同时使用SpringBoot、XXL-JOB和Quartz搭建的任务调度双引擎方案。这套方案不是简单把两个调度器堆在一起,而是先做了选型分析,再把“本地任务”和“分布式任务”拆开,最后才组建成一套高可用调度平台。如果你正面临Quartz和XXL-JOB二选一的问题,或者已经选了其中一个但发现有些场景不好收场,这篇文章应该能帮你省掉不少排查时间。
文章会覆盖三块内容:为什么同时保留两个引擎、SpringBoot集成Quartz的完整流程、SpringBoot集成XXL-JOB的完整流程,以及双引擎落地后那些从日志里才能翻出来的问题。我尽量把参数怎么定、代码怎么写、集群怎么配都说清楚,也会把配置不当引发的重复执行、任务丢失、JobHandler找不到这类问题列成速查表。整个内容基于我实际操作的SpringBoot 2.x/3.x环境,部分版本差异会单独标注。
1. 双引擎选型思路:为什么不是二选一,而是“本地+平台”并存
1.1 两个调度器的本质差异:嵌入式调度器与独立调度平台
Quartz本质上是一个调度库,它嵌入在你的业务进程里。调度逻辑、线程池、任务存储都在你的应用内部,哪怕你把它配成集群模式,底层也是靠数据库锁来协调多个节点。它的优势是轻、无额外依赖、和SpringBoot结合非常自然;劣势是没有管理界面,任务状态不直观,分布式协调能力比较弱,多个节点跑同一个任务时很容易出现“谁抢到锁谁执行”的微妙行为。
XXL-JOB则是一个完整的分布式任务调度平台,部署上分成调度中心和执行器两大部分。调度中心是独立的Web服务,提供可视化界面,你可以在上面配置任务、查看调度日志、手动触发;执行器是嵌入你业务服务里的一个组件,负责向调度中心注册,然后接收调度指令并执行具体任务。它的优势是分布式能力强,有路由策略、失败重试、阻塞处理、动态调整,适合多实例、需要运维介入的场景;劣势是额外引入了一套服务和数据库表,部署和运维成本比Quartz高。
我们当时选型的时候,团队里有人提出“直接全换XXL-JOB”,也有人坚持“Quartz够用了”。后来我把任务清单拉出来逐一对比,才意识到这不是一个非此即彼的问题。相当一部分任务是服务本地的、不关心其他节点,比如清理某个本地临时目录、预热本机缓存,这些任务如果用XXL-JOB,还要专门注册执行器、配置路由,纯属浪费。而另一些任务涉及订单超时关单、多服务间的数据同步,必须保证全局只有一次执行,出了问题还要能在后台看到日志,这类任务用Quartz的集群锁虽然也能做,但可观测性和重试能力都差很多。
1.2 为什么最终保留了双引擎而不是二选一
我最终定下的方案是:Quartz负责“进程内任务”,XXL-JOB负责“跨实例的分布式任务”。这个划分不是拍脑袋,而是根据任务属性来的。
简单来说,一个任务满足以下任一条件,我就会放到XXL-JOB上:
- 必须保证在多个业务实例中只有一个实例执行,且需要明确的执行记录;
- 执行结果需要给非开发者查看,或者需要人工在页面上手动触发;
- 需要失败重试、超时控制、阻塞处理等策略;
- 任务依赖多个服务的数据,或者执行频率比较高,需要调度中心统一管理。
反之,满足以下条件的任务继续留在Quartz:
- 任务只在本地进程内执行,比如清理本地缓存目录、刷新本地配置;
- 任务不需要精确的全局互斥,即便某个实例没执行也不会造成业务影响;
- 不想为了一个小任务去配置执行器注册和路由策略;
- 本地开发环境直接能跑,不需要依赖调度中心。
这样做的直接好处是:调度中心的压力小了很多,不会出现几百个任务全部在XXL-JOB上被频繁扫描的情况;Quartz这边也保留了一部分轻量任务,即使XXL-JOB调度中心短暂不可用,本地任务也不会跟着中断。坏处是系统里同时存在两套调度基础,排查问题时需要分清当前任务走的是哪条链路。但我觉得这个代价是值得的,因为“本地+平台”分别做到了轻量和可控。
1.3 什么情况下可以只保留一个引擎
如果你是刚开始做项目,或者任务量在二三十个以内,我不建议直接上双引擎。先想想你的业务规模,如果所有任务都在同一个服务里,实例也就一两个,Quartz加数据库持久化完全够用。XXL-JOB带来的管理便利在这个阶段还不明显,反而增加了一套服务需要维护。
反过来,如果你所在团队运维能力比较强,项目一开始就规划了多个微服务,任务数量也较多,那直接全部使用XXL-JOB也可以。它的执行器本身支持很多种任务类型,内置的调度逻辑比Quartz在分布式场景下要完善得多,你不需要自己去处理数据库锁、节点抢占这些细节。我一贯的建议是:双引擎是过渡方案也是工程折衷,它不是银弹。只有当任务结构已经足够复杂,单一引擎在某些方面显得别扭时,双引擎才真正有价值。选型时多考虑团队维护成本和业务形态,比单纯比技术功能更重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高可用调度平台的整体架构设计
2.1 整体拓扑与核心组件
高性能调度平台不能只盯着调度器本身,它牵扯到存储层、调度中心、执行器、本地调度、负载均衡等多个部分。我这里把实际落地后的模块列出来,供你参考搭建时的边界。
- 调度中心集群:部署多套xxl-job-admin实例,共用同一个MySQL数据库,前面挂nginx做负载均衡。
- 存储层:MySQL存储调度任务、调度日志、执行器注册信息。Quartz集群时也使用JDBC JobStore,表结构与XXL-JOB的库表分开。
- 执行器集群:业务服务部署多个节点,每个节点内同时集成XXL-JOB执行器SpringBoot组件和Quartz调度器。
- 本地任务区:Quartz负责的进程内任务,只存在于单个业务节点,不做跨节点协调。
- 监控与告警:对调度中心的HTTP健康检查、任务执行失败率、Quartz线程池活跃度做基础监控。
这套结构里,最需要注意的一点是:XXL-JOB调度中心虽然是集群部署,但它不是传统意义上的无状态服务。调度中心节点的状态几乎都落在数据库里,包括锁、任务信息、调度日志。所以数据库的高可用比调度中心本身的负载均衡更关键。如果MySQL挂了,调度中心前端再健康,任务调度也会停摆。我们在部署时把调度中心数据库单独拆了出来,没有和业务库混在一起,避免业务大查询拖垮调度表。
2.2 调度中心高可用与执行器集群的互相配合
XXL-JOB调度中心的高可用实现相对简单,因为调度中心节点通过数据库中的xxl_job_lock表来保证同一时刻只有一个节点在竞争调度任务。多个admin节点启动后,哪个节点拿到锁,哪个节点就负责在当前时间帧内触发任务,其他节点处于待命状态。调度中心可以横向扩展,但数据库必须撑得住。
执行器高可用则取决于任务的路由策略。在XXL-JOB后台的任务配置里,路由策略有很多种,常用的是“轮询”“一致性HASH”“故障转移”。我通常给核心任务配置“故障转移”,它的逻辑是调度中心在触发任务时按顺序尝试执行器地址,如果第一个失败就转移到下一个。这样即便某个业务实例重启,任务也会被自动分发到健康的实例上。需要配合另一个参数:执行器的注册方式。执行器启动后会自动向调度中心注册自己的IP和端口,多个实例会注册成多个地址,调度中心这边天然能看到一个执行器下的多台机器。
2.3 双引擎的任务分类与命名规划
既然系统里同时存在Quartz和XXL-JOB,任务命名就绝对不能乱。我们内部定了一套规则:Quartz相关配置类统一放在com.xxx.config.quartz,任务类放在com.xxx.job.local;XXL-JOB的JobHandler统一放在com.xxx.job.distributed,类名后缀必须带JobHandler。后台界面上创建任务时,任务名称统一带前缀,比如本地任务就叫LOCAL_xxx,分布式任务就叫DIST_xxx。这样出问题时,看一眼任务名基本就能判断该去查哪套日志。
命名规则虽然简单,但对双引擎排障非常有帮助。有一段时间我们线上有个任务偶尔不执行,排查时先在XXL-JOB后台调度日志里看到状态是“成功”,但业务侧确认数据没有变化,最后发现是一个同样命名的Quartz任务在另一个服务实例上覆盖了数据处理逻辑。如果一开始命名就区分好,这种问题一眼就能看到。
3. 核心实操:SpringBoot集成Quartz并实现本地任务调度
3.1 依赖引入与基础配置
SpringBoot集成Quartz现在非常方便,官方已经提供了starter。我在项目里用的是SpringBoot 2.7.x版本,依赖如下:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
如果你用的是SpringBoot 3.x,这个依赖照样有效,但要留意Quartz本身对javax和jakarta命名空间的兼容。SpringBoot 3.x和Quartz的版本搭配需要仔细看官方说明,否则启动时可能因为类找不到而报错。
接下来在application.yml里配置Quartz的线程池和存储方式。如果是单机RAM存储,配置很简单:
yaml复制spring:
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
properties:
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 15000
org.quartz.jobStore.tablePrefix: QRTZ_
这里job-store-type设为jdbc表示任务持久化到数据库,initialize-schema: always会让Quartz自动建表,但生产环境我更建议手动执行官方SQL脚本,并改成never,防止每次启动都去检查表结构。threadCount控制Quartz调度线程池大小,默认10就够了,但如果任务很多且执行时间较长,可以适当调到20或30。
注意:Quartz的线程池和业务线程池是两回事。Quartz线程池负责执行任务逻辑,如果任务里又调用了阻塞式接口,线程很容易被占满,后续任务就会排队等待。
3.2 定义Job、Trigger和Scheduler
Quartz里的三个核心概念是JobDetail、Trigger和Scheduler。SpringBoot里我习惯用配置类统一创建。
先写一个业务任务类,继承QuartzJobBean:
java复制@Component
public class LocalCleanJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) {
// 执行本地临时目录清理
// 注意:这里通过Spring容器注入的Service是可以用的
}
}
然后定义一个配置类,创建JobDetail和Trigger:
java复制@Configuration
public class QuartzConfig {
@Bean
public JobDetail localCleanJobDetail() {
return JobBuilder.newJob(LocalCleanJob.class)
.withIdentity("localCleanJob")
.storeDurably(true)
.build();
}
@Bean
public Trigger localCleanJobTrigger() {
return TriggerBuilder.newTrigger()
.forJob(localCleanJobDetail())
.withIdentity("localCleanJobTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
.build();
}
}
这里storeDurably(true)表示即使没有Trigger关联,JobDetail也会被保存。实际项目里,我建议每个任务都显式配置一个Trigger,这样数据库里能直观看到任务的触发规则,排查问题时也更方便。Cron表达式建议写在一个常量类里,不要散落在各个配置类中,方便统一调整。
3.3 Quartz集群模式与数据库表结构
当业务服务部署多个实例,而且希望Quartz任务全局只执行一次时,需要开启集群模式。上面的配置已经开了isClustered: true,同时要确保多个实例连接同一个数据库。Quartz会通过持久化JobStore里的锁表来协调调度,具体来说就是多个实例在触发任务前会对数据库中的QRTZ_LOCKS表执行SELECT ... FOR UPDATE,谁拿到锁谁就执行本次触发。
Quartz集群需要初始化11张表,包括QRTZ_JOB_DETAILS、QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS、QRTZ_SIMPLE_TRIGGERS、QRTZ_BLOB_TRIGGERS、QRTZ_CALENDARS、QRTZ_PAUSED_TRIGGER_GRPS、QRTZ_FIRED_TRIGGERS、QRTZ_SCHEDULER_STATE、QRTZ_LOCKS、QRTZ_JOB_LISTENERS和QRTZ_TRIGGER_LISTENERS。具体脚本在Quartz官方发布的包里,每个数据库厂商都有对应版本。
集群模式下有一个容易踩的坑:所有节点的时间必须尽量一致,最好都配置NTP时间同步。Quartz的检查机制依赖实例之间的时间差,如果某台机器时钟偏了,集群内会出现任务触发错乱,甚至频繁抢锁。另外,clusterCheckinInterval这个参数我习惯设成15秒,不要设太短,否则高并发情况下数据库压力会增加。
3.4 Spring容器与Quartz整合的经典坑
Quartz创建Job实例是通过自身的JobFactory,默认情况下它不认识Spring的@Autowired注解。也就是说,如果你直接在LocalCleanJob里注入一个@Autowired的Service,运行时会发现它是null。解决办法是让Spring的AutowireCapableBeanFactory接管Quartz的Job实例化过程。
在SpringBoot中,可以注入一个SpringBeanJobFactory,或者更简单地,在配置类里使用AutowireCapableBeanJobFactory。示例:
java复制@Bean
public SchedulerFactoryBean quartzScheduler(ApplicationContext applicationContext) {
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
AutowireCapableBeanJobFactory jobFactory = new AutowireCapableBeanJobFactory(applicationContext.getAutowireCapableBeanFactory());
jobFactory.setIgnoredUnknownProperties("applicationContext");
factoryBean.setJobFactory(jobFactory);
return factoryBean;
}
我最初就忘记配这个,导致任务跑起来后service一直报NPE,查了半天才发现是Quartz的Job并不归Spring管理。如果你在任务类里用了不少Spring组件,这一步务必加上。
4. 核心实操:XXL-JOB调度中心与执行器集成SpringBoot
4.1 调度中心的本地部署与基础配置
XXL-JOB的调度中心本质是一个SpringBoot应用,叫xxl-job-admin。你可以从GitHub下载对应版本的源码,然后用Maven打包,或者直接部署官方提供的发行包。我在本地测试时习惯直接改配置后运行jar。
需要先初始化数据库,官方SQL脚本在/doc/db/tables_xxl_job.sql。数据库里包含调度任务、执行器注册、调度日志等表,整个平台的所有状态都靠它。执行完脚本后,修改application.properties里的数据源配置,设置一个调度中心访问用的accessToken,这个token后续所有执行器都要保持一致,否则执行器注册不上。
调度中心默认端口是8080,我一般会改成单独端口,比如8088。启动后访问管理后台,默认账号密码是admin/123456,正式环境务必改掉。后台首页展示调度任务的执行统计,也是排查问题最常用的人口。
提示:调度中心版本和
xxl-job-core版本必须严格一致。比如调度中心是2.4.0,业务服务里的xxl-job-core也要用2.4.0。版本不一致会出现在线任务注册异常、JobHandler找不到等诡异问题。
4.2 业务服务接入执行器
业务服务里接入执行器分三步:引入依赖、写配置、写配置类注册执行器。
xml复制<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>
配置项如下:
yaml复制xxl:
job:
admin:
addresses: http://localhost:8088/xxl-job-admin
accessToken: your-token
executor:
appname: order-service-executor
address:
ip:
port: 9999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
appname是执行器在调度中心里的唯一标识,后面在管理后台添加执行器时会用到。port是执行器接收调度请求的端口,默认9999,这个端口是执行器内嵌的HTTP服务端口,不要和业务端口混淆。address和ip一般留空,自动注册即可。生产环境多网卡时,一定要显式指定对外可访问的IP,否则调度中心会收到内网地址导致无法触发。
然后写一个配置类,向Spring容器注册XxlJobSpringExecutor:
java复制@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}")
private int port;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appname);
executor.setPort(port);
executor.setAccessToken(accessToken);
return executor;
}
}
启动业务服务后,到调度中心后台的“执行器管理”里添加执行器,填上相同的appname。等你看到执行器出现了在线节点,就说明注册成功了。
4.3 创建任务、JobHandler编写与核心参数解析
在业务服务里,用@XxlJob注解写任务方法:
java复制@Component
public class OrderTimeoutJobHandler {
@XxlJob("orderTimeoutHandler")
public ReturnT<String> handle(String param) {
// 处理订单超时关单逻辑
return ReturnT.SUCCESS;
}
}
然后在调度中心后台的“任务管理”里新建任务。执行器选刚刚注册的那个,JobHandler填orderTimeoutHandler,Cron表达式按需填写。
这里有几个参数非常关键,我逐个说:
- 路由策略:多实例时指定任务分发给哪个执行器节点。“轮询”适合实例处理能力均衡的任务,“一致性HASH”适合需要将相同参数路由到同一实例的任务,“故障转移”适合对单次失败容忍度更低的任务。
- 阻塞处理策略:任务执行中如果到了下一次触发时间,是“单机串行”还是“丢弃后续调度”,我一般选“单机串行”,两个相同任务不会同时涌入同一台机器。
- 失败重试次数:不是越大越好。如果任务本身有幂等性,可以设置2-3次;如果任务执行时间较长且下游接口不稳定,重试反而可能放大问题。
- 超时时间:非常推荐设置,比如300秒。任务执行超过这个时间会被调度中心标记为失败。
- 任务参数:可以在后台传入一个字符串参数,JobHandler方法里的
String param会收到。适合把同一套Handler复用到多个场景。
这些参数是XXL-JOB区别于Quartz的重要优势,Quartz也有misfire策略,但管理和监控远没有这么直观。
4.4 调度中心集群与执行器集群的高可用组合
前面说过调度中心可以集群部署。实际操作时,打包两个xxl-job-admin实例,共用同一个数据库,然后在nginx里配置负载均衡,指向这两个实例即可。需要注意,两个实例的accessToken必须一样,否则执行器在向不同调度中心注册时会被拒绝。
执行器集群则简单得多。只要业务服务是多节点部署,且每个节点都注册到同一个执行器appname下,调度中心就会在节点列表里看到多个地址。配合“故障转移”路由策略,即使某个节点宕机,调度中心也能把任务转到另一个节点。
我在生产环境遇到过一个问题:执行器注册了两个节点,但任务一直只在一个节点上执行。后来发现是路由策略选了“第一个”,这个策略固定取第一个在线地址,如果第一个地址一直处于健康状态,其他节点自然不会被使用。后面我把核心任务改成“故障转移”,问题才解决。如果你想把某一类任务均摊到所有实例上,路由策略选“轮询”更合适。
5. 双引擎落地中的问题与排查
5.1 任务重复执行:这是最让人头疼的问题
双引擎系统里,任务重复执行的原因往往被放大。Quartz集群模式下,如果某个节点在触发任务时发生了数据库锁等待超时,可能会在下一个检查周期再次触发;XXL-JOB这边,如果你把路由策略设置成“轮询”而多个执行器节点没有做幂等控制,同一个任务也会在不同节点上各执行一次。
我处理这类问题的顺序是:先确认任务是否具备幂等性,如果没有,先在任务入口加去重逻辑,比如用数据库唯一键或Redis分布式锁;然后看调度日志,确定是哪个执行器在哪个时间点执行了。对于Quartz任务,我会在Job里打上实例IP和线程ID的日志,这样能快速定位是哪个节点执行的。对于XXL-JOB任务,直接看调度日志和每个执行器的日志即可。
经验:任务幂等是所有调度系统的最后一道防线。不管选Quartz、XXL-JOB还是双引擎,任务本身必须设计成可重复执行且结果一致,否则换任何调度器都无法彻底解决重复问题。
5.2 Quartz集群与XXL-JOB共用一个数据库的隐患
很多团队为了省事,会让Quartz的QRTZ表和XXL_JOB的调度表放在同一个MySQL实例里。这样在任务量不大时没问题,但一旦任务密集,两个调度器都会频繁访问数据库,容易互相影响。我在一次压测中就发现Quartz集群的锁等待时间变长,同时XXL-JOB调度日志写入变慢。
最好还是把两套调度表放在不同的数据库实例,或者使用独立的MySQL服务。如果实在只能共库,至少给Quartz表和XXL_JOB表分别设置独立的连接池,不要让它们共用业务系统的默认连接池。这里有一个小技巧:在SpringBoot的配置里,可以用两个DataSource,一个给业务,一个给调度组件,避免调度器的长时间锁占住业务连接。
5.3 JobHandler找不到、执行器注册不上、日志不输出
这几个问题在XXL-JOB接入时很典型。
- JobHandler找不到:确认
@XxlJob注解里的名称和后台任务配置的JobHandler完全一致,注意大小写和空格。 - 执行器注册不上:看执行器启动日志里有没有
xxl-job register相关语句,然后用后台的“执行器管理”查看在线节点。常见原因是accessToken不一致,或者admin.addresses地址端口错误。 - 调度中心触发任务后日志不输出:先检查执行器端口是否能通。很多服务器只开放了业务端口,忘了开放执行器端口,触发请求根本进不来。再检查
logpath目录是否存在且可写,否则日志文件无法生成。
Quartz这边也有类似问题。我们经常遇到本地任务在单个节点跑得好好的,部署多实例后反而不断被触发。排查思路是先看每个节点是否使用的同一张QRTZ表,再看isClustered是否每个实例都开了。如果有一个节点忘记加集群配置,它就会和集群里的节点抢占任务,造成重复执行。
5.4 常见问题速查表
我在实际运维中把高频问题整理成了表格,方便值班同事直接对照处理。
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| Quartz任务不触发 | 线程池被长任务占满 | 调大threadCount,排查任务里的阻塞调用 |
| Quartz集群重复执行 | 部分节点未配置isClustered=true | 检查所有节点配置,保证时钟一致 |
| XXL-JOB执行器不在线 | accessToken不一致或端口被防火墙拦截 | 核对token,放行执行器端口 |
| JobHandler not found | 注解名称拼写不一致 | 核对后台JobHandler和注解值 |
| 调度中心任务一直显示“调度中” | 管理员节点未拿到锁或数据库连接耗尽 | 重启调度中心,检查数据库连接池 |
| 任务成功但业务无变化 | 路由策略导致多节点均未执行 | 检查调度日志和业务日志,确认执行节点 |
| 任务超时被终止 | 任务实际执行时间超过超时时间 | 调大超时时间或优化任务逻辑 |
6. 运维经验与优化方向
6.1 双引擎共存时的任务分域管理
一开始我提到任务分域,但真正执行时还需要配套的治理手段。我在项目里会维护一份“任务清单”,是一个简单的Markdown表格,记录任务编号、任务名称、所属引擎、执行器、Cron表达式、负责人和备注。每次新增任务都要先在清单里登记,再编写代码和配置。这个清单看起来原始,但比任何后台管理界面都直接。排障时打开清单,谁负责什么、任务归属哪个引擎,一目了然。
任务分域还有一个容易被忽略的好处:容量规划。Quartz线程池大小和XXL-JOB执行器线程池大小的调整,都需要知道每个任务大概占多长时间。我们会在日志里定期统计任务耗时,把超过10秒的任务视为“长任务”,这类任务单独设置线程池参数,避免它们挤占短任务的执行。
6.2 高可用调度平台的监控与告警
调度平台本身是高可用的,但调度任务不会自己告诉你它失败了几次。我现在的做法是,针对XXL-JOB,定期拉取调度日志里失败次数超过阈值的任务,然后推送告警到钉钉或企微;针对Quartz,自定义一个JobListener,在任务执行异常时抛出告警信息。这样双引擎的失败都能第一时间被感知。
另外,数据库的慢查询监控也不能遗漏。Quartz集群和XXL-JOB的数据访问都不复杂,但锁等待和日志写入量大时,很容易出现慢SQL。把QRTZ_LOCKS和xxl_job_log相关的慢查询阈值调整到500ms,如果出现超出,就要检查是不是表数据量太大,该做归档了。
6.3 一些值得记住的优化细节
第一个是版本兼容。SpringBoot版本不能太激进,尤其是3.x刚出来那年,很多第三方库还没适配,调度组件也很容易出问题。现在虽然情况好很多,但我依然建议上线前先做一轮兼容性验证。第二个是执行器端口规划。业务服务中执行器端口要固定下来,不要用自动端口,否则服务重启后端口漂移,调度中心会注册到旧地址。第三个是Quartz任务日志必须打印实例标识。多节点部署后,没有日志里的IP信息,很难排查是哪个节点执行了任务。第四个是XXL-JOB的路由策略要和任务幂等性配合。如果你用“轮询”,所有节点必须都能处理同类任务,并且处理时做好互斥;如果用“一致性HASH”,要留意节点变化后路由结果会改变。
我个人在实际操作中最大的体会是:任务调度系统的复杂度通常不会被业务代码体现,它藏在那些“偶发不执行”“偶尔重复跑”的疑难问题里。Quartz和XXL-JOB双引擎不是说技术越复杂越好,而是通过合理的任务分域,让每个任务走最适合它的调度链路。如果项目初期任务量不大,就从Quartz开始,把表结构和运行机制吃透;等项目规模真正上去,再引入XXL-JOB补足分布式调度和可视化管理。最后再分享一个小技巧:在Quartz的Job和XXL-JOB的Handler里,统一加一个traceId或任务实例ID到日志中,后续无论查日志还是对账,都会轻松很多。
