You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot多实例下如何确保定时任务仅单实例执行?

解决Spring Boot多实例下定时任务仅单实例执行的问题

要实现多实例环境下定时任务仅单个实例执行,且任务完成后在间隔周期内无其他实例重复执行,核心是要把分布式锁和执行时间校验结合起来——不能只靠执行期间的锁,还要确保即使锁释放后,其他实例也会判断是否满足间隔条件再决定是否执行。以下是几种可行的实现方案:

方案1:基于Redis的分布式锁+执行时间记录

利用Redis存储两个关键信息:任务执行锁、上次任务完成时间戳。任务触发时,先获取锁,再校验是否已过间隔周期,满足条件才执行任务。

代码示例

@Component
public class SingleInstanceScheduledTask {
    private static final String TASK_LOCK_KEY = "task:lock:dataSync";
    private static final String TASK_LAST_EXECUTE_KEY = "task:lastExecute:dataSync";
    private static final long INTERVAL_MILLIS = 3600000; // 1小时间隔

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Scheduled(fixedRate = INTERVAL_MILLIS)
    public void executeSyncTask() {
        // 获取分布式锁,锁过期时间设为任务最长执行时间的2倍,避免任务未完成锁提前释放
        Boolean lockAcquired = redisTemplate.opsForValue()
                .setIfAbsent(TASK_LOCK_KEY, "locked", 2, TimeUnit.HOURS);

        if (Boolean.TRUE.equals(lockAcquired)) {
            try {
                String lastExecuteStr = redisTemplate.opsForValue().get(TASK_LAST_EXECUTE_KEY);
                long currentTime = System.currentTimeMillis();

                // 首次执行或已过设定间隔周期
                if (lastExecuteStr == null || currentTime - Long.parseLong(lastExecuteStr) >= INTERVAL_MILLIS) {
                    // 执行核心任务逻辑
                    System.out.println("执行定时任务,实例:" + InetAddress.getLocalHost().getHostName());
                    // 更新上次执行时间
                    redisTemplate.opsForValue().set(TASK_LAST_EXECUTE_KEY, String.valueOf(currentTime));
                } else {
                    System.out.println("未到间隔周期,跳过执行,实例:" + InetAddress.getLocalHost().getHostName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                redisTemplate.delete(TASK_LOCK_KEY);
            }
        } else {
            System.out.println("获取锁失败,跳过执行,实例:" + InetAddress.getLocalHost().getHostName());
        }
    }
}

关键注意点

  • 锁的过期时间必须大于任务最长执行时间,防止任务未完成锁就失效导致重复执行;
  • 若任务执行时间可能超过间隔周期,可调整fixedRate为fixedDelay,确保任务完成后再开始计算间隔。

方案2:使用Quartz集群模式

Quartz原生支持集群部署,通过共享数据库存储任务元数据,自动选举单个实例执行任务,天然避免多实例重复执行。

配置步骤

  1. 添加Quartz依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  1. 配置Quartz数据源(所有实例共用同一数据库):
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=always
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000
  1. 定义Job和Trigger:
@Component
public class SyncJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 执行核心任务逻辑
        System.out.println("Quartz集群任务执行,实例:" + InetAddress.getLocalHost().getHostName());
    }
}

@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail syncJobDetail() {
        return JobBuilder.newJob(SyncJob.class)
                .withIdentity("syncJob")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger syncJobTrigger() {
        // 固定间隔触发,任务完成后间隔1小时再执行
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInHours(1)
                .repeatForever()
                .withMisfireHandlingInstructionNextWithExistingCount();

        return TriggerBuilder.newTrigger()
                .forJob(syncJobDetail())
                .withIdentity("syncTrigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

优势

  • 无需手动实现分布式锁,Quartz自动处理集群节点的任务调度;
  • 支持任务失败重试、错过触发时间的补偿策略。

方案3:基于数据库的行级锁+执行时间记录

通过数据库表存储任务状态,利用行级锁(SELECT ... FOR UPDATE)确保同一时间只有一个实例能获取任务执行权限,同时校验间隔周期。

数据库表设计

CREATE TABLE task_schedule (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    task_name VARCHAR(50) UNIQUE NOT NULL COMMENT '任务名称',
    last_execute_time BIGINT NOT NULL COMMENT '上次执行时间戳',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

代码示例

@Component
public class DbLockScheduledTask {
    private static final String TASK_NAME = "dataSync";
    private static final long INTERVAL_MILLIS = 3600000;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Scheduled(fixedRate = INTERVAL_MILLIS)
    public void executeTask() {
        jdbcTemplate.execute((Connection conn) -> {
            // 行级锁,锁定当前任务的记录
            try (PreparedStatement pstmt = conn.prepareStatement(
                    "SELECT last_execute_time FROM task_schedule WHERE task_name = ? FOR UPDATE")) {
                pstmt.setString(1, TASK_NAME);
                ResultSet rs = pstmt.executeQuery();
                long currentTime = System.currentTimeMillis();

                if (rs.next()) {
                    long lastExecuteTime = rs.getLong("last_execute_time");
                    if (currentTime - lastExecuteTime >= INTERVAL_MILLIS) {
                        // 执行任务逻辑
                        System.out.println("数据库锁方案执行任务,实例:" + InetAddress.getLocalHost().getHostName());
                        // 更新上次执行时间
                        jdbcTemplate.update(
                                "UPDATE task_schedule SET last_execute_time = ? WHERE task_name = ?",
                                currentTime, TASK_NAME);
                    }
                } else {
                    // 首次执行,插入初始记录
                    jdbcTemplate.update(
                            "INSERT INTO task_schedule(task_name, last_execute_time) VALUES (?, ?)",
                            TASK_NAME, currentTime);
                    System.out.println("首次执行任务,实例:" + InetAddress.getLocalHost().getHostName());
                }
                return null;
            }
        });
    }
}

关键注意点

  • 必须使用事务包裹锁操作和任务执行,避免锁释放后任务执行失败导致状态不一致;
  • 适合本身已使用数据库的项目,无需额外引入Redis等中间件。

内容的提问来源于stack exchange,提问作者idrak_07

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 12:10:49