Spring + Quartz 集群模式,配置 Cron `0/15 * * * * ?` (每 15 秒) 觸發任務。啟動多實例時,控制台顯示多個線程(`DefaultQuartzScheduler_Worker-1` 等)同時執行同一任務,導致業務邏輯重複執行。改為 1 分鐘間隔則正常。
# 錯誤配置
org.quartz.jobStore.misfireThreshold = 60000 # 60 秒
# 任務間隔 15 秒 < 60 秒,導致每次觸發都被判定為 misfire
原理: Quartz 若發現任務預計觸發時間已超過 `misfireThreshold`,會觸發誤火處理機制。集群下多節點搶鎖、恢復任務,極易導致重複執行。
配置中雖有 `@DisallowConcurrentExecution`,但僅防止單 JVM 內並發,集群環境下多節點各自加載 Job,註解不生效。需依賴 Quartz 集群機制(數據庫行鎖 `SELECT ... FOR UPDATE`)保證同一時刻只有一節點獲取 Trigger。
若多節點啟動過快、數據庫寫入 `QRTZ_SCHEDULER_STATE` 有延遲,可能出現重複 `instanceId`,導致調度器誤以為是同一節點重啟,觸發恢復邏輯。
# ===== 調度器標識 =====
org.quartz.scheduler.instanceName = AUTO_REPEAT_CLUSTER
org.quartz.scheduler.instanceId = AUTO # 確保集群內唯一,依賴 DB 主鍵
# ===== JobStore 集群核心 =====
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 15000 # 15秒心跳,檢測失效節點
org.quartz.jobStore.misfireThreshold = 5000 # 【關鍵】設為 5秒,小於最小調度間隔(15秒)
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myDS
# ===== 線程池 =====
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
# ===== 數據源 =====
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://...?useSSL=false
org.quartz.dataSource.myDS.user = xxx
org.quartz.dataSource.myDS.password = xxx
org.quartz.dataSource.myDS.maxConnections = 20
<bean id="autoRepeatRequestTask" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.xxx.job.AutoRequestRepeatJob" />
<property name="durability" value="true" /> <!-- 非觸發時也保留 -->
<property name="requestsRecovery" value="true" /> <!-- 調度器重啟時恢復未完成任務 -->
</bean>
<bean id="autoRepeatRequestDoTime" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="autoRepeatRequestTask" />
<property name="cronExpression" value="0/15 * * * * ?" /> <!-- 每15秒 -->
<property name="misfireInstructionName" value="MISFIRE_INSTRUCTION_FIRE_ONCE_NOW" /> <!-- 誤火策略:立即執行一次 -->
</bean>
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="configLocation" value="classpath:quartz.properties" />
<property name="triggers">
<list><ref bean="autoRepeatRequestDoTime" /></list>
</property>
<property name="overwriteExistingJobs" value="true" /> <!-- 覆蓋舊配置,防止重複註冊 -->
<property name="startupDelay" value="30" /> <!-- 錯峰啟動,減少集群啟動衝突 -->
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
</bean>
@PersistJobDataAfterExecution
@DisallowConcurrentExecution // 單節點內防並發
public class AutoRequestRepeatJob extends QuartzJobBean {
private static final Logger log = LoggerFactory.getLogger(AutoRequestRepeatJob.class);
@Override
protected void executeInternal(JobExecutionContext context) {
// 業務層面冪等性設計(如 Redis 分佈式鎖、數據庫唯一索引)
String lockKey = "job:lock:" + context.getJobDetail().getKey().getName();
if (!redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
log.warn("任務已在別處執行,跳過本次");
return;
}
try {
// 核心業務邏輯
requestRepeatService.autoRepeatRequest();
} finally {
redisTemplate.delete(lockKey);
}
}
}
| 參數 | 錯誤值 | 修正值 | 說明 |
|---|---|---|---|
| misfireThreshold | 60000 (60s) | 5000 (5s) | 必須 < 最小調度間隔 |
| clusterCheckinInterval | 15000 | 15000 | 心跳間隔,建議 10-30s |
| startupDelay | 10 | 30 | 錯峰啟動,避免搶鎖風暴 |
| overwriteExistingJobs | false | true | 防止重複部署產生殘留 Job |
| misfireInstruction | 默認 | FIRE_ONCE_NOW | 誤火時補償執行一次,而非丟棄 |
短間隔任務重複執行的核心原因是 `misfireThreshold` 設置過大導致誤火判定失效,集群鎖競爭下恢復邏輯觸發重複執行。修正閾值、配置誤火策略、添加業務層冪等鎖、錯峰啟動,四管齊下徹底解決。原則:**調度間隔 > misfireThreshold > 網絡抖動容忍度**。