You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Spring MVC中实现多实例Scheduler同步?避免多服务器重复执行

多实例环境下Spring MVC Scheduler同步的解决方案

这确实是分布式部署下定时任务的经典坑——多实例同时跑同一个调度,轻则重复执行浪费资源,重则搞乱数据。我在几个项目里都碰到过这个问题,给你分享几个实际用过的靠谱方案:

1. 基于数据库锁的轻量实现

如果你的项目已经依赖数据库,这是最容易上手的方案,不需要额外引入组件。

核心思路

利用数据库的排他锁特性,任务执行前先“抢锁”:只有成功锁定任务记录的实例才能执行,其他实例直接跳过。

具体做法

  1. 建一张任务锁表,字段大概包括:task_name(任务唯一标识)、lock_status(锁定状态)、instance_id(持有锁的实例标识)、lock_expire_time(锁过期时间)。
  2. 任务触发时,用SELECT ... FOR UPDATE(排他锁)查询对应任务记录:
    • 如果能查到且未被锁定,就更新锁定状态、实例ID和过期时间,然后执行任务;
    • 如果记录已被锁定或过期时间未到,直接退出任务。
  3. 任务执行完成后,记得释放锁(更新锁定状态为未锁定)。

代码示例(伪代码)

@Scheduled(cron = "0 0 1 * * ?")
public void dailyStatTask() {
    String taskName = "daily-stat-task";
    String instanceId = InetAddress.getLocalHost().getHostName();
    
    // 尝试获取锁,带超时自动释放逻辑
    boolean lockAcquired = taskLockMapper.tryLock(taskName, instanceId, 300);
    if (!lockAcquired) {
        log.info("任务{}已被其他实例执行,当前实例跳过", taskName);
        return;
    }
    
    try {
        // 执行你的业务逻辑
        statDailyData();
    } finally {
        // 释放锁
        taskLockMapper.releaseLock(taskName, instanceId);
    }
}

优缺点

  • ✅ 优点:实现简单,无额外依赖,中小项目足够用
  • ❌ 缺点:高并发下可能有数据库性能损耗,需做好锁超时机制防止实例挂死导致锁无法释放

2. 基于Redis分布式锁的高性能实现

如果项目已经引入Redis,这个方案性能更好,适合高频率执行的任务。

核心思路

利用Redis的原子性操作(SETNXSET命令带NX/EX参数)实现分布式锁:因为Redis是单线程的,同一时间只有一个实例能成功设置锁键。

具体做法

  1. 任务触发时,尝试设置一个唯一的锁键(比如scheduler:lock:daily-stat),同时设置过期时间(防止实例挂死锁不释放)。
  2. 如果设置成功,说明抢到锁,执行任务;设置失败则直接退出。
  3. 任务执行完成后,只有持有锁的实例才能删除锁键(用UUID作为锁值,删除前校验值是否匹配,防止误删其他实例的锁)。

代码示例(Spring Data Redis)

@Autowired
private StringRedisTemplate redisTemplate;

@Scheduled(cron = "0 0 1 * * ?")
public void dailyStatTask() {
    String lockKey = "scheduler:lock:daily-stat";
    String lockValue = UUID.randomUUID().toString();
    // 设置锁,5分钟过期
    Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, 5, TimeUnit.MINUTES);
    
    if (locked == null || !locked) {
        log.info("其他实例正在执行任务,当前实例跳过");
        return;
    }
    
    try {
        // 执行业务逻辑
        statDailyData();
    } finally {
        // 校验锁值,只有当前实例持有锁才删除
        String currentValue = redisTemplate.opsForValue().get(lockKey);
        if (lockValue.equals(currentValue)) {
            redisTemplate.delete(lockKey);
        }
    }
}

进阶优化

如果任务执行时间可能超过锁的过期时间,可以用Redisson的可重入锁,它自带自动续期机制,能避免锁提前释放导致的重复执行。

优缺点

  • ✅ 优点:性能优异,Redis响应快,适合高并发场景
  • ❌ 缺点:需要维护Redis集群,需处理锁超时和续期问题

3. 基于Quartz集群的成熟方案

如果你的项目已经在用Quartz做任务调度,直接开启它的集群模式即可,无需自己写锁逻辑。

核心思路

Quartz会把任务的状态、调度信息和锁存在共享数据库中,多个Quartz节点会自动协调:同一时间只有一个节点执行任务,故障节点的任务会自动转移到其他节点。

具体做法

  1. 配置Quartz的集群参数:
    • 设置org.quartz.jobStore.isClustered = true
    • 所有实例使用同一个Quartz数据库(官方提供了初始化表结构的SQL脚本)
  2. 在Spring MVC中配置SchedulerFactoryBean时,注入这些集群属性。

代码示例(Spring配置类)

@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource quartzDataSource) {
    SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
    Properties quartzProps = new Properties();
    
    quartzProps.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
    quartzProps.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
    quartzProps.put("org.quartz.jobStore.dataSource", "quartzDataSource");
    quartzProps.put("org.quartz.jobStore.isClustered", "true");
    quartzProps.put("org.quartz.jobStore.clusterCheckinInterval", "20000"); // 节点心跳间隔
    
    scheduler.setQuartzProperties(quartzProps);
    scheduler.setDataSource(quartzDataSource);
    // 注册你的JobDetail和Trigger
    scheduler.setJobDetails(dailyStatJobDetail());
    scheduler.setTriggers(dailyStatTrigger());
    
    return scheduler;
}

优缺点

  • ✅ 优点:成熟稳定,自带故障转移、任务持久化等功能,支持复杂调度需求
  • ❌ 缺点:配置相对繁琐,需要维护Quartz的数据库表,适合已有Quartz依赖的项目

4. 基于分布式调度框架的一站式方案

如果你的项目有大量分布式任务需求,或者需要监控、告警、任务分片等高级功能,直接用专门的分布式调度框架更省心。

常用框架

  • XXL-JOB:开箱即用,配置简单,自带Web管理界面,支持任务调度、监控、告警、分片执行等
  • Elastic-Job:基于ZooKeeper实现,支持任务分片、弹性扩容,适合复杂分布式场景

核心思路

部署一个独立的调度中心,所有应用实例作为执行器注册到调度中心,由调度中心统一分配任务,确保同一时间只有一个执行器执行任务。

示例(XXL-JOB)

  1. 部署XXL-JOB调度中心
  2. 在Spring MVC项目中引入XXL-JOB执行器依赖
  3. 在任务方法上添加@XxlJob注解:
    @XxlJob("dailyStatTask")
    public void dailyStatTask() throws Exception {
        // 执行业务逻辑
        statDailyData();
    }
    
  4. 在调度中心配置任务的Cron表达式、执行器等参数即可

优缺点

  • ✅ 优点:功能全面,开箱即用,支持监控、告警、分片等高级特性
  • ❌ 缺点:需要引入额外框架,增加系统复杂度,适合中大型分布式项目

总结选择建议

  • 中小项目、已有数据库:优先选数据库锁
  • 已有Redis、任务执行频率高:选Redis分布式锁
  • 已有Quartz依赖:直接开启Quartz集群模式
  • 大型分布式项目、需要高级功能:选分布式调度框架

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

火山引擎 最新活动