在Spring Boot 3中實現分布式定時任務,確保多實例環境下任務僅執行一次,可以采用以下方案:
方案一:Redis分布式鎖(推薦)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.concurrent.TimeUnit;@Component
public class DistributedScheduler {private final StringRedisTemplate redisTemplate;private static final String LOCK_KEY = "TASK_LOCK:MY_TASK";private static final int LOCK_TIMEOUT = 9; // 鎖超時時間(分鐘)public DistributedScheduler(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}@Scheduled(cron = "0 */10 * * * *")public void scheduledTask() {Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "locked", Duration.ofMinutes(LOCK_TIMEOUT));if (lockAcquired != null && lockAcquired) {try {// 執行任務邏輯performTask();} finally {// 任務完成后手動釋放鎖(可選)// redisTemplate.delete(LOCK_KEY);}}}private void performTask() {// 具體任務代碼System.out.println("Task executed at: " + new Date());}
}
依賴配置:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
關鍵點:
- 使用
setIfAbsent
原子性操作獲取鎖,避免并發問題。 - 設置鎖的自動過期時間(略小于任務間隔),防止死鎖。
- 根據業務需求選擇是否手動釋放鎖(如任務執行時間可能超過鎖超時時間)。
方案二:數據庫樂觀鎖
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;@Component
public class DatabaseLockScheduler {@Scheduled(cron = "0 */10 * * * *")@Transactionalpublic void scheduledTask() {// 1. 查詢最近一次任務記錄TaskLock lastLock = taskLockRepository.findTopByTaskNameOrderByExecuteTimeDesc("MY_TASK");// 2. 檢查是否已執行過if (lastLock != null && lastLock.getExecuteTime().isAfter(LocalDateTime.now().minusMinutes(10))) {return;}// 3. 插入新記錄(利用唯一約束或版本號控制并發)TaskLock newLock = new TaskLock("MY_TASK", LocalDateTime.now());taskLockRepository.save(newLock);// 執行任務邏輯performTask();}
}
實體類示例:
@Entity
public class TaskLock {@Idprivate String taskName;private LocalDateTime executeTime;@Versionprivate Integer version;// 省略構造方法/getter/setter
}
關鍵點:
- 使用數據庫唯一約束(復合唯一索引)或版本號控制并發。
- 需要處理可能的異常(如唯一約束沖突)。
方案三:Quartz集群模式
配置步驟:
- 添加依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
- 配置數據庫存儲(
application.properties
):
spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000
- 定義任務:
public class MyJob implements Job {@Overridepublic void execute(JobExecutionContext context) {// 任務邏輯}
}
- 配置調度器:
@Configuration
public class QuartzConfig {@Beanpublic JobDetail jobDetail() {return JobBuilder.newJob(MyJob.class).withIdentity("myTask").storeDurably().build();}@Beanpublic Trigger trigger() {return TriggerBuilder.newTrigger().forJob(jobDetail()).withSchedule(CronScheduleBuilder.cronSchedule("0 */10 * * * ?")).build();}
}
方案對比
方案 | 優點 | 缺點 |
---|---|---|
Redis鎖 | 實現簡單,性能高 | 依賴Redis,需處理鎖續期問題 |
數據庫鎖 | 無需額外中間件 | 數據庫壓力大,需處理并發沖突 |
Quartz集群 | 官方集群支持,功能強大 | 配置復雜,依賴數據庫表結構 |
選擇建議:
- 輕量級場景優先使用Redis鎖
- 已有數據庫基礎設施可考慮數據庫鎖
- 復雜調度需求選擇Quartz集群