Skip to content

Quartz 集群原理

单机 Quartz 够用,但你总有一天会需要集群。

问题是:多台服务器的 Quartz 怎么协调?谁来触发任务?怎么保证任务不重复执行?

今天,我们来彻底搞清楚 Quartz 集群的原理。

为什么需要集群?

┌─────────────────────────────────────────────────────────────┐
│                    单机 vs 集群                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   单机模式:                                                │
│   ┌──────────────┐                                         │
│   │   Server     │                                         │
│   │ [调度器]      │                                         │
│   │ [执行器]      │                                         │
│   └──────────────┘                                         │
│                                                             │
│   问题:                                                   │
│   · 单点故障(服务器挂了,任务全停)                         │
│   · 无法水平扩展(处理能力有限)                             │
│                                                             │
│   ─────────────────────────────────────────────             │
│                                                             │
│   集群模式:                                                │
│   ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│   │   Server1    │  │   Server2    │  │   Server3    │      │
│   │ [调度器]      │  │ [调度器]      │  │ [调度器]      │      │
│   │ [执行器]      │  │ [执行器]      │  │ [执行器]      │      │
│   └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│          │                 │                 │              │
│          └─────────────────┼─────────────────┘              │
│                            ▼                                │
│                   ┌────────────────┐                       │
│                   │     数据库      │                       │
│                   │  (协调中心)     │                       │
│                   └────────────────┘                       │
│                                                             │
│   优势:                                                   │
│   · 高可用(一台挂了,其他继续)                             │
│   · 负载均衡(任务分摊到多台)                               │
│   · 可水平扩展                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Quartz 集群架构

Quartz 集群使用 JDBC JobStore,所有调度状态都存储在数据库中。

┌─────────────────────────────────────────────────────────────┐
│                    Quartz 集群架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌────────────┐  ┌────────────┐  ┌────────────┐          │
│   │  Node 1    │  │  Node 2    │  │  Node 3    │          │
│   │ ┌────────┐ │  │ ┌────────┐ │  │ ┌────────┐ │          │
│   │ │Scheduler│ │  │ │Scheduler│ │  │ │Scheduler│ │          │
│   │ └────┬───┘ │  │ └────┬───┘ │  │ └────┬───┘ │          │
│   │      │      │  │      │      │  │      │      │          │
│   └──────┼──────┘  └──────┼──────┘  └──────┼──────┘          │
│          │                 │                 │              │
│          └─────────────────┼─────────────────┘              │
│                            ▼                                │
│   ┌────────────────────────────────────────────────────┐  │
│   │                    数据库                            │  │
│   │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │  │
│   │  │ QRTZ_    │  │ QRTZ_    │  │ QRTZ_    │          │  │
│   │  │ JOBS     │  │ TRIGGERS │  │ LOCKS    │          │  │
│   │  │          │  │          │  │          │          │  │
│   │  └──────────┘  └──────────┘  └──────────┘          │  │
│   │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │  │
│   │  │ QRTZ_    │  │ QRTZ_    │  │ QRTZ_    │          │  │
│   │  │ CALENDARS│  │ JOB_     │  │ SCHEDULER│          │  │
│   │  │          │  │ DETAILS  │  │ STATE    │          │  │
│   │  └──────────┘  └──────────┘  └──────────┘          │  │
│   └────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心表结构

Quartz 集群依赖以下关键表:

表名用途
QRTZ_JOB_DETAILS存储 Job 信息
QRTZ_TRIGGERS存储 Trigger 信息
QRTZ_CALENDARS存储日历信息
QRTZ_LOCKS分布式锁
QRTZ_SCHEDULER_STATE各节点状态

分布式锁:QRTZ_LOCKS

这是集群协调的核心表:

sql
CREATE TABLE QRTZ_LOCKS (
    LOCK_NAME VARCHAR(40) PRIMARY KEY
);

-- 预定义的锁
INSERT INTO QRTZ_LOCKS VALUES ('TRIGGER_ACCESS');
INSERT INTO QRTZ_LOCKS VALUES ('JOB_ACCESS');
INSERT INTO QRTZ_LOCKS VALUES ('CALENDAR_ACCESS');
INSERT INTO QRTZ_LOCKS VALUES ('STATE_ACCESS');
INSERT INTO QRTZ_LOCKS VALUES ('MISFIRE_ACCESS');

节点状态:QRTZ_SCHEDULER_STATE

sql
CREATE TABLE QRTZ_SCHEDULER_STATE (
    SCHEDULER_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    LAST_CHECKIN_TIME BIGINT NOT NULL,
    CHECKIN_INTERVAL BIGINT NOT NULL,
    PRIMARY KEY (SCHEDULER_NAME, INSTANCE_NAME)
);

每个节点定期更新自己的检查时间,其他节点通过这个表判断节点是否存活。

集群工作原理

1. 节点启动

┌─────────────────────────────────────────────────────────────┐
│                    节点启动流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 节点从数据库加载所有 JobDetail 和 Trigger               │
│                                                             │
│   2. 节点将自身注册到 QRTZ_SCHEDULER_STATE                   │
│      ┌─────────────────────────────┐                        │
│      │ QRTZ_SCHEDULER_STATE       │                        │
│      ├─────────────────────────────┤                        │
│      │ instance_name | last_checkin│                        │
│      │ Node1        | 1700000000   │                        │
│      │ Node2        | 1700000001   │                        │
│      │ Node3        | 1700000002   │                        │
│      └─────────────────────────────┘                        │
│                                                             │
│   3. 节点启动调度线程,开始监听触发时间                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. 任务触发:竞争获取锁

┌─────────────────────────────────────────────────────────────┐
│                    任务触发流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   假设:Trigger T1 到达触发时间                             │
│                                                             │
│   ┌────────────┐  ┌────────────┐  ┌────────────┐          │
│   │  Node 1   │  │  Node 2   │  │  Node 3   │          │
│   │ 检测到T1  │  │ 检测到T1  │  │ 检测到T1  │          │
│   │ 到达时间  │  │ 到达时间  │  │ 到达时间  │          │
│   └─────┬──────┘  └─────┬──────┘  └─────┬──────┘          │
│         │                │                │                │
│         ▼                ▼                ▼                │
│   ┌────────────────────────────────────────────┐           │
│   │              尝试获取锁                     │           │
│   │         INSERT INTO QRTZ_LOCKS...          │           │
│   │                                             │           │
│   │   谁先插入成功,谁就获得锁                   │           │
│   └────────────────────────────────────────────┘           │
│                            │                                │
│                            ▼                                │
│                   ┌────────────────┐                       │
│                   │   数据库锁      │                       │
│                   │  (悲观锁)       │                       │
│                   └────────────────┘                       │
│                                                             │
│   结果:只有一个节点能获得锁,执行任务                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. 获取锁后执行

java
// Quartz 内部伪代码
public class ClusterManager {
    
    public void triggerJob(Trigger trigger) {
        // 1. 获取锁
        Connection conn = getConnection();
        conn.beginTransaction();
        
        try {
            // 2. 更新 Trigger 状态为 ACQUIRED
            updateTriggerState(trigger, STATE_ACQUIRED);
            
            // 3. 提交事务,释放锁
            conn.commit();
        } finally {
            conn.close();
        }
        
        // 4. 在本地执行任务
        executeJob(trigger);
    }
}

4. 节点故障检测

┌─────────────────────────────────────────────────────────────┐
│                    故障检测机制                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   每个节点定期检查其他节点状态:                              │
│                                                             │
│   1. 从 QRTZ_SCHEDULER_STATE 读取所有节点                    │
│                                                             │
│   2. 计算距离上次检查时间:                                   │
│      current_time - last_checkin_time                      │
│                                                             │
│   3. 如果超过阈值(默认 30 秒),判定节点失联                 │
│                                                             │
│   ┌──────────────────────────────────────────────────┐     │
│   │ 节点失联后的处理:                                   │     │
│   │ · 重新触发该节点正在执行的任务(MISFIRE)            │     │
│   │ · 清理该节点的持久化数据                            │     │
│   │ · 任务被其他节点接管                               │     │
│   └──────────────────────────────────────────────────┘     │
│                                                             │
│   注意:必须设置合理的 CHECKIN_INTERVAL                      │
│   太长 → 故障检测延迟                                        │
│   太短 → 数据库压力大                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

配置集群模式

yaml
spring:
  quartz:
    job-store-type: jdbc
    
    properties:
      org:
        quartz:
          scheduler:
            instanceName: MyClusterScheduler
            instanceId: AUTO  # 自动生成实例 ID
          
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true  # 启用集群模式
            clusterCheckinInterval: 10000  # 检查间隔 10 秒
            useProperties: false
          
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5

关键配置

配置说明建议值
isClustered启用集群true
instanceName集群实例名称,同一组要相同自定义
instanceId实例 IDAUTO
clusterCheckinInterval节点状态检查间隔10000-20000ms
usePropertiesJobDataMap 使用字符串 keytrue

集群 vs 非集群

维度非集群集群
高可用
故障转移
负载均衡✅(争抢模式)
数据库要求可选必须
性能略低(锁竞争)
配置复杂度

集群的局限性

局限性一:不是真正的负载均衡

┌─────────────────────────────────────────────────────────────┐
│            Quartz 集群不是真正的负载均衡                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   假设:100 个任务,3 个节点                                 │
│                                                             │
│   理想情况:                                                 │
│   │ Node1: 33 个任务                                        │
│   │ Node2: 33 个任务                                        │
│   │ Node3: 34 个任务                                        │
│                                                             │
│   实际情况:                                                │
│   │ 任务触发后,节点竞争锁                                    │
│   │ 获得锁的节点执行任务                                     │
│   │ 其他节点等待                                             │
│                                                             │
│   结果:执行快的节点会抢到更多任务                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

局限性二:锁竞争影响性能

节点越多,锁竞争越激烈。大量任务同时触发时,数据库可能成为瓶颈。

局限性三:不支持任务分片

如果需要将一个大任务分成多个小任务并行处理,Quartz 集群做不到。需要 XXL-Job 或 ElasticJob。

总结

概念说明
协调方式数据库(JDBC JobStore)
分布式锁QRTZ_LOCKS 表
故障检测QRTZ_SCHEDULER_STATE
任务分配竞争获取锁
故障转移节点失联后任务重新触发

Quartz 集群解决了高可用问题,但不是负载均衡解决方案。

思考题

如果 3 个节点的集群中,Node1 在执行任务时突然断电,会发生什么?

任务会被恢复吗?数据会丢失吗?

这涉及到 Quartz 的故障恢复机制。

基于 VitePress 构建