SpringBoot定时任务多实例互斥执行的完整指南

 更新时间:2026年01月26日 08:33:31   作者:风象南  
Spring Boot 的 @Scheduled 写定时任务很方便,但多实例部署时有个问题,同一个定时任务会在每台机器上都触发执行,下面我们就来看看如何解决吧

Spring Boot 的 @Scheduled 写定时任务很方便,但多实例部署时有个问题:同一个定时任务会在每台机器上都触发执行。

比如部署了两台应用服务器,凌晨 2 点的数据统计任务会同时跑两遍,数据重复、文件重复生成。

解决这个问题通常有几种思路。

常见方案

方案一:单机执行

只在一台指定的机器上跑任务:

@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
    String hostname = InetAddress.getLocalHost().getHostName();
    if (!"app-server-01".equals(hostname)) {
        return;
    }
    // do something
}

问题很明显:那台机器挂了,任务就不执行了。

方案二:Redis 分布式锁

用 Redis 的 SETNX 实现互斥(或者Redission):

@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
    Boolean locked = stringRedisTemplate.opsForValue()
        .setIfAbsent("task:data-sync", "1", 10, TimeUnit.MINUTES);
    if (Boolean.FALSE.equals(locked)) {
        return;
    }
    try {
        // do something
    } finally {
        stringRedisTemplate.delete("task:data-sync");
    }
}

能用,但每个任务都要写一遍加锁释放逻辑,而且需要项目里有 Redis。

ShedLock 方案

ShedLock 是一个专门解决定时任务重复执行问题的框架,支持多种存储后端(数据库、Redis、MongoDB 等)。

核心思路:在存储层记录每个任务的锁状态,任务执行前先抢锁,抢到了才执行。

集成步骤

1. 添加依赖

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.42.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>4.42.0</version>
</dependency>

2. 创建锁表

CREATE TABLE shedlock (
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP(3) NOT NULL,
    locked_at TIMESTAMP(3) NOT NULL,
    locked_by VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

3. 配置 LockProvider

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {

    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withDataSource(dataSource)
                .withTableName("shedlock")
                .build()
        );
    }
}

4. 给定时任务加注解

@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "dataSyncTask", lockAtMostFor = "5m")
public void syncData() {
    // do something
}

完成。多台服务器部署时,只有抢到锁的那台会执行任务。

注解参数说明

@SchedulerLock 有几个关键参数:

name:锁名称,相同 name 的任务会互斥执行。建议用任务名来命名,保持唯一。

lockAtMostFor:锁的最大持有时间。

这是为了防止任务执行过程中机器宕机,导致锁永远不释放。比如设置 5m,即使任务执行超时,锁也会在 5 分钟后自动过期。

一般按任务预期执行时间的 2 倍左右设置,留些余量。

lockAtLeastFor:锁的最小持有时间。

这是为了防止任务执行太快,锁立即释放被其他机器抢到。

比如定时任务每分钟执行一次,任务 5 秒就跑完了。如果没有这个参数,其他机器可能会在同一分钟内再次抢到锁执行。

这种情况下可以设置 lockAtLeastFor = "1m",确保锁保持到下一分钟。

实现原理

ShedLock 的实现逻辑不复杂。

任务执行前,会向数据库插入或更新锁记录:

INSERT INTO shedlock (name, lock_until, locked_at, locked_by)
VALUES ('dataSyncTask', '2025-01-25 02:05:00', NOW(), '192.168.1.10')
ON DUPLICATE KEY UPDATE
  lock_until = '2025-01-25 02:05:00',
  locked_at = NOW(),
  locked_by = '192.168.1.10'
WHERE lock_until < NOW();

关键是最后的 WHERE lock_until < NOW() 条件:

  • 如果当前锁已过期,UPDATE 成功,抢到锁
  • 如果当前锁未过期,UPDATE 影响行数为 0,抢锁失败,任务不执行

任务执行完成后不需要主动释放锁,等待 lock_until 时间到期即可。

适用场景

ShedLock 的定位很明确:专门为定时任务设计的分布式锁框架。

适合

  • 定时任务需要互斥执行,避免重复
  • 希望用注解方式简化锁的代码逻辑
  • 需要自动锁过期机制,防止死锁

不适合

  • 高并发抢锁的业务场景(比如秒杀、库存扣减),ShedLock 不是为此设计
  • 需要可重入锁、读写锁等复杂特性
  • 需要精确控制锁获取和释放时机的业务逻辑

ShedLock 和通用分布式锁是互补关系,不是替代关系。如果你的业务代码里需要手动加锁解锁,用 Redisson 或手动实现 Redis SETNX 更合适。但如果只是为了解决定时任务重复执行的问题,ShedLock 是更简洁的方案。

几个注意事项

1. 存储后端选择

本文演示用的是 JDBC 方式(基于数据库表)。ShedLock 还支持 Redis、MongoDB、ZooKeeper、Hazelcast 等多种存储后端,根据项目现有技术栈选择即可。

2. 表名自定义(数据库存储时)

如果用 JDBC 作为存储后端,默认表名是 shedlock,可以按需修改: 3. 机器标识

locked_by 字段记录是哪台机器拿到的锁,默认是主机名。可以自定义成更有意义的标识:

.withLockedByValue("app-" + getServerIp())

4. 主从/多数据源场景(数据库存储时)

如果项目有多个数据源,确保 ShedLock 用的是主库,避免主从延迟导致的锁问题。

总结

ShedLock 是一个专门为定时任务设计的分布式锁框架:

  • 优点:注解式使用、集成简单、自动锁过期、支持多种存储后端
  • 局限性:只适用于定时任务场景,不适用于通用业务加锁

和手动写 Redis 分布式锁相比,ShedLock 把定时任务锁的逻辑抽象出来了,代码更简洁。但如果你需要的是通用业务锁,还是用 Redisson 或手写 SETNX 更合适。

以上就是SpringBoot定时任务多实例互斥执行的完整指南的详细内容,更多关于SpringBoot定时任务的资料请关注脚本之家其它相关文章!

相关文章

  • Java中Thread类基本用法详解

    Java中Thread类基本用法详解

    Java中的Thread类是用于创建和管理线程的类,Thread类提供了许多方法来管理线程,包括启动线程、中断线程、暂停线程等,下面这篇文章主要给大家介绍了关于Java中Thread类基本用法的相关资料,需要的朋友可以参考下
    2023-06-06
  • Jedis出现connection timeout问题解决方法(JedisPool连接池使用实例)

    Jedis出现connection timeout问题解决方法(JedisPool连接池使用实例)

    这篇文章主要介绍了Jedis出现connection timeout问题解决方法,使用Jedis的JedisPool连接池解决了这个问题,需要的朋友可以参考下
    2014-05-05
  • springboot集成mybatisPlus+多数据源的实现示例

    springboot集成mybatisPlus+多数据源的实现示例

    这篇文章主要介绍了springboot集成mybatisPlus+多数据源的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • Mybatis如何实现延迟加载及缓存

    Mybatis如何实现延迟加载及缓存

    这篇文章主要介绍了Mybatis如何实现延迟加载及缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • 通过Java实现反向代理集群服务的平滑分配

    通过Java实现反向代理集群服务的平滑分配

    这篇文章主要介绍了如何通过Java语言,自己编写的平滑加权轮询算法,结合线程池和Socket 网络编程等,并实现反向代理集群服务的平滑分配,需要的可以参考一下
    2022-04-04
  • Springboot中如何通过yml为实体类注入属性

    Springboot中如何通过yml为实体类注入属性

    这篇文章主要介绍了Springboot中如何通过yml为实体类注入属性,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • idea maven依赖引入失效无法正常导入依赖问题的解决方法

    idea maven依赖引入失效无法正常导入依赖问题的解决方法

    有时候idea导入一个新项目,或者pom文件修改(新增)了依赖,pom文件和代码会报红,提示依赖包不存在,下面这篇文章主要给大家介绍了关于idea maven依赖引入失效无法正常导入依赖问题的解决方法,需要的朋友可以参考下
    2023-04-04
  • Java Swing组件BoxLayout布局用法示例

    Java Swing组件BoxLayout布局用法示例

    这篇文章主要介绍了Java Swing组件BoxLayout布局用法,结合实例形式分析了Swing使用BoxLayout容器进行布局的相关方法与操作技巧,需要的朋友可以参考下
    2017-11-11
  • 利用Spring框架为自己的校园卡充值(推荐)

    利用Spring框架为自己的校园卡充值(推荐)

    这篇文章主要介绍了利用Spring框架为自己的校园卡充值,本次实验主要运用了Spring的控制反转(IOC)和依赖注入(DI)等知识,通过利用Spring框架编写Java程序,实现学生卡的单次充值,如需对学生卡进行多次充值,可将部分代码修改即可实现,需要的朋友可以参考下
    2022-10-10
  • 关于IDEA MybatisX插件的使用小技巧

    关于IDEA MybatisX插件的使用小技巧

    这篇文章主要介绍了关于MybatisX插件的使用小技巧,MybatisX是IDEA的一款第三方插件,可以极大地提升我们的开发效率,一起来看看吧
    2023-03-03

最新评论