如何在Spring MVC中实现多实例Scheduler同步?避免多服务器重复执行
多实例环境下Spring MVC Scheduler同步的解决方案
这确实是分布式部署下定时任务的经典坑——多实例同时跑同一个调度,轻则重复执行浪费资源,重则搞乱数据。我在几个项目里都碰到过这个问题,给你分享几个实际用过的靠谱方案:
1. 基于数据库锁的轻量实现
如果你的项目已经依赖数据库,这是最容易上手的方案,不需要额外引入组件。
核心思路
利用数据库的排他锁特性,任务执行前先“抢锁”:只有成功锁定任务记录的实例才能执行,其他实例直接跳过。
具体做法
- 建一张任务锁表,字段大概包括:
task_name(任务唯一标识)、lock_status(锁定状态)、instance_id(持有锁的实例标识)、lock_expire_time(锁过期时间)。 - 任务触发时,用
SELECT ... FOR UPDATE(排他锁)查询对应任务记录:- 如果能查到且未被锁定,就更新锁定状态、实例ID和过期时间,然后执行任务;
- 如果记录已被锁定或过期时间未到,直接退出任务。
- 任务执行完成后,记得释放锁(更新锁定状态为未锁定)。
代码示例(伪代码)
@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的原子性操作(SETNX或SET命令带NX/EX参数)实现分布式锁:因为Redis是单线程的,同一时间只有一个实例能成功设置锁键。
具体做法
- 任务触发时,尝试设置一个唯一的锁键(比如
scheduler:lock:daily-stat),同时设置过期时间(防止实例挂死锁不释放)。 - 如果设置成功,说明抢到锁,执行任务;设置失败则直接退出。
- 任务执行完成后,只有持有锁的实例才能删除锁键(用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节点会自动协调:同一时间只有一个节点执行任务,故障节点的任务会自动转移到其他节点。
具体做法
- 配置Quartz的集群参数:
- 设置
org.quartz.jobStore.isClustered = true - 所有实例使用同一个Quartz数据库(官方提供了初始化表结构的SQL脚本)
- 设置
- 在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)
- 部署XXL-JOB调度中心
- 在Spring MVC项目中引入XXL-JOB执行器依赖
- 在任务方法上添加
@XxlJob注解:@XxlJob("dailyStatTask") public void dailyStatTask() throws Exception { // 执行业务逻辑 statDailyData(); } - 在调度中心配置任务的Cron表达式、执行器等参数即可
优缺点
- ✅ 优点:功能全面,开箱即用,支持监控、告警、分片等高级特性
- ❌ 缺点:需要引入额外框架,增加系统复杂度,适合中大型分布式项目
总结选择建议
- 中小项目、已有数据库:优先选数据库锁
- 已有Redis、任务执行频率高:选Redis分布式锁
- 已有Quartz依赖:直接开启Quartz集群模式
- 大型分布式项目、需要高级功能:选分布式调度框架
内容的提问来源于stack exchange,提问作者user7621942




