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原生支持集群部署,通过共享数据库存储任务元数据,自动选举单个实例执行任务,天然避免多实例重复执行。
配置步骤
- 添加Quartz依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
- 配置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
- 定义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
相关产品推荐
相关产品推荐

