Quartz集群案例

wen java案例 2

本文目录导读:

Quartz集群案例

  1. 目录导读
  2. Quartz集群的痛点与挑战:单点故障的生死时速
  3. 核心原理:数据库级分布式锁与节点协同机制
  4. 实战案例:电商订单超时关闭系统(含代码级配置)
  5. 集群故障恢复与性能调优秘笈
  6. 高频问答:解决你部署时的“拦路虎”

Quartz集群实战案例:从单点故障到高可用任务调度架构的完整演进

目录导读

  1. Quartz集群的痛点与挑战:为什么单机调度撑不住生产环境?
  2. 核心原理:数据库级分布式锁与集群节点协同机制
  3. 实战案例:电商订单超时关闭系统(含代码级配置)
  4. 集群故障恢复与性能调优秘笈
  5. 高频问答:解决你部署时的“拦路虎”

Quartz集群的痛点与挑战:单点故障的生死时速

场景还原:某电商平台凌晨2点,300万条未支付订单需要批量关闭,原本运行良好的单节点Quartz调度器突然宕机——CPU飙升至100%,JVM内存溢出,所有定时任务陷入停滞,业务方电话轰炸,而运维只能手动重启,损失惨重。

核心痛点

  • 单点故障:调度器挂了,所有任务“陪葬”
  • 任务重复执行:多节点部署时,同一任务被多次触发,造成数据错乱
  • 水平扩展困难:无法通过增加节点提升调度吞吐量

解决方案思路:Quartz官方提供的集群方案,通过数据库共享状态,实现“多个调度器实例,一套任务调度状态”。


核心原理:数据库级分布式锁与节点协同机制

集群架构三要素

  1. 共享数据库:所有节点连接同一个Quartz相关表(如QRTZ_TRIGGERSQRTZ_JOB_DETAILS
  2. 悲观锁机制:节点在执行任务前,对QRTZ_LOCKS表对应行执行SELECT ... FOR UPDATE,获取分布式锁
  3. 故障自动转移QRTZ_FIRED_TRIGGERS表记录正在执行的任务,宕机节点实例名被标记,其他节点接管

关键配置表结构(简化版):

-- 锁表:控制集群节点并发访问
CREATE TABLE QRTZ_LOCKS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME VARCHAR(40) NOT NULL,
    PRIMARY KEY (SCHED_NAME, LOCK_NAME)
);
-- 已触发任务表:记录执行状态,用于故障恢复
CREATE TABLE QRTZ_FIRED_TRIGGERS (
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    FIRED_TIME BIGINT NOT NULL,
    ...
);

工作原理流程图

graph TD
A[节点1获取锁] --> B[执行任务]
C[节点2尝试获取锁] --> D[等待/跳过]
B --> E[释放锁]
D --> A

实战案例:电商订单超时关闭系统(含代码级配置)

业务需求:每30秒扫描超过30分钟未支付的订单,执行关闭操作,要求做到“一次订单只能被一个节点处理”。

环境:Spring Boot 2.7 + Quartz 2.3.2 + MySQL 8.0 + 双节点部署

Step 1:Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

Step 2:核心配置(application.yml)

spring:
  quartz:
    job-store-type: jdbc # 必须使用JDBC存储
    jdbc:
      initialize-schema: always # 首次自动建表
    properties:
      org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
      org.quartz.jobStore.isClustered: true # 开启集群模式
      org.quartz.jobStore.clusterCheckinInterval: 15000 # 心跳检测间隔(ms)
      org.quartz.scheduler.instanceName: ClusterScheduler # 集群实例名
      org.quartz.scheduler.instanceId: AUTO # 节点自动生成唯一ID
      org.quartz.threadPool.threadCount: 10 # 每个节点线程池大小

Step 3:任务实现类

@DisallowConcurrentExecution // 防止同一Job并发执行
public class OrderTimeoutJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 1. 查询超时订单(带分页,每批500条)
        // 2. 调用订单服务关闭,更新状态
        // 3. 记录耗时日志
        System.out.println("节点-" + context.getScheduler().getSchedulerInstanceId() 
                         + " 处理订单中... 当前时间:" + new Date());
    }
}

Step 4:启动类注入调度器

@Configuration
public class QuartzConfig {
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setQuartzProperties(quartzProperties());
        return factory;
    }
}

验证运行效果:启动两个节点后,查看数据库QRTZ_SCHEDULER_STATE表,发现两个节点均已注册,当节点1执行任务时,节点2的日志显示“线程阻塞等待锁”,手动kill节点1,节点2在15秒内自动接管剩余任务。


集群故障恢复与性能调优秘笈

故障恢复实战

案例:某金融系统节点间时间偏差过大,导致任务误判。 解决方案

  • 使用NTP同步所有节点时间,偏差必须<1000ms
  • 调大org.quartz.jobStore.clusterCheckinInterval到20000(默认15秒),容忍网络抖动

性能调优三板斧

  1. 线程池配置threadCount设置为节点CPU核数×2,避免资源浪费
  2. 批量获取任务:设置org.quartz.scheduler.batchTriggerAcquisitionMaxCount=100,减少数据库往返
  3. 优化SQL锁策略:关闭org.quartz.jobStore.dbRetryInterval重试间隔,默认15000ms,可降低为5000ms提升故障转移速度

监控告警

使用Prometheus+Grafana监控:

  • 指标:qrtz_fired_triggers表行数、节点心跳时间
  • 告警:当某个节点心跳超过30秒未更新,立即通知运维

高频问答:解决你部署时的“拦路虎”

Q1:为什么我的Quartz集群会出现任务重复执行? A:最常见原因是@DisallowConcurrentExecution注解缺失,当任务执行时间超过repeatInterval时,多个节点会同时抢到同一trigger,务必为所有Job添加该注解,并保证数据库连接池有足够的活跃连接数(建议≥5)。

Q2:集群模式下,如何指定某类任务只在特定节点执行? A:使用JobDataMap传参,在Job内判断当前节点实例ID。

String targetNode = context.getMergedJobDataMap().getString("targetNode");
if (!targetNode.equals(context.getScheduler().getSchedulerInstanceId())) {
    return; // 非目标节点直接跳过
}

Q3:节点宕机后,正在运行的任务卡死怎么办? A:Quartz本身无法检测任务是否真正完成,建议在Job中增加超时逻辑,配合qrtz_fired_triggers表判断,如果任务超过5分钟未完成,手动更新该记录状态,或借助Kill脚本重启节点。

Q4:数据库选型有什么坑? A:强烈不建议使用SQL Server,其隔离级别默认下,SELECT FOR UPDATE有时不能正确阻塞,导致任务并发执行,MySQL/PostgreSQL均无此问题,务必设置连接池最大空闲时间小于数据库wait_timeout,避免连接失效。

Q5:集群扩容时,需要手动清理旧表数据吗? A:不需要,新增节点启动时,会自动注册到QRTZ_SCHEDULER_STATE,但注意:如果旧节点异常宕机,其状态记录可能会保留,可在部署脚本中增加清理SQL:

DELETE FROM QRTZ_SCHEDULER_STATE WHERE LAST_CHECKIN_TIME < NOW() - INTERVAL 1 DAY;

上一篇XXL-JOB案例

下一篇Elastic-Job案例

抱歉,评论功能暂时关闭!