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 | 实例 ID | AUTO |
| clusterCheckinInterval | 节点状态检查间隔 | 10000-20000ms |
| useProperties | JobDataMap 使用字符串 key | true |
集群 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 的故障恢复机制。
