在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 * * * *")
@Transactional
public 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 {
@Id
private String taskName;
private LocalDateTime executeTime;
@Version
private 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 {
@Override
public void execute(JobExecutionContext context) {
// 任务逻辑
}
}
- 配置调度器:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
return JobBuilder.newJob(MyJob.class)
.withIdentity("myTask")
.storeDurably()
.build();
}
@Bean
public Trigger trigger() {
return TriggerBuilder.newTrigger()
.forJob(jobDetail())
.withSchedule(CronScheduleBuilder.cronSchedule("0 */10 * * * ?"))
.build();
}
}
方案对比
方案 | 优点 | 缺点 |
---|---|---|
Redis锁 | 实现简单,性能高 | 依赖Redis,需处理锁续期问题 |
数据库锁 | 无需额外中间件 | 数据库压力大,需处理并发冲突 |
Quartz集群 | 官方集群支持,功能强大 | 配置复杂,依赖数据库表结构 |
选择建议:
- 轻量级场景优先使用Redis锁
- 已有数据库基础设施可考虑数据库锁
- 复杂调度需求选择Quartz集群