分布式多节点定时任务怎么只执行一次

问题:

当我们有多个服务器,每个服务器上都有相同的定时任务代码时,比如每天凌晨定时插入数据。如果多个服务器上的定时任务都执行了会导致数据的重复。
解决办法:1、@SchedulerLock实现;2、 基于Redis的分布式锁;

1、@SchedulerLock实现;

Shedlock库可以确保你的定时任务最多同时执行一次。如果一个任务正在一个节点上执行,它会获取一个锁,以防止从另一个节点(或线程)执行相同的任务。如果一个任务已经在一个节点上执行,则在其他节点上的执行不会等待,只需跳过它即可 。

导入坐标:

  compile('net.javacrumbs.shedlock:shedlock-provider-jdbc-template:2.1.0')
   compile('net.javacrumbs.shedlock:shedlock-spring:2.2.0')
// Maven方式
   <dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-spring</artifactId>
       <version>2.1.0</version>
   </dependency>
   <dependency>
       <groupId>net.javacrumbs.shedlock</groupId>
       <artifactId>shedlock-provider-jdbc-template</artifactId>
       <version>2.2.0</version>
   </dependency>

在数据库里加上创建提供锁的外部存储表(shedlock)

CREATE TABLE shedlock(
    name VARCHAR(64) ,
    lock_until TIMESTAMP(3) NULL,
    locked_at TIMESTAMP(3) NULL,
    locked_by  VARCHAR(255),
    PRIMARY KEY (name)
)
属性 说明
name 锁名称 name必须是主键
lock_until 释放锁时间
locked_at 获取锁时间
locked_by 锁提供者

//@SchedulerLock声明的锁名称自动创建对应的键值对,提供锁。@SchedulerLock(name = “scheduledTaskName”)name值对应的是库里的name的赋值。
DB形式的外部存储需要创建表结构,redis等非结构形式的外部存储template会根据@SchedulerLock声明的锁名称自动创建对应的键值对,提供锁。

配置类:

@Component
public class ShedLockConfig {
    
    

    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
    
    
        return new JdbcTemplateLockProvider(dataSource);
    }

    @Bean
    public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
    
    
        return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider)
                .withPoolSize(10)
                .withDefaultLockAtMostFor(Duration.ofMinutes(10))
                .build();
    }

}

启用SchedulerLock,使用@EnableSchedulerLock注释将库集成到Spring中。

import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
// 开启定时任务注解
@EnableScheduling
// 开启定时任务锁,默认设置锁最大占用时间为30s
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
@MapperScan(basePackages="cn.pilipa.finance.salary.persistence")
@ServletComponentScan
public class SalaryApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(SalaryApplication.class, args);
    }
}

在定时任务上添加@SchedulerLock注解

@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
    
    
   // do something
}

SchedulerLock 参数

  • @SchedulerLock
    只有带注释的方法被锁定,库忽略所有其他计划的任务。您还必须指定锁的名称。同一时间只能执行一个任务。
  • name
    分布式锁名称,注意 锁名称必须唯一。
  • lockAtMostFor & lockAtMostForString
    指定在执行节点死亡时应将锁保留多长时间。这只是一个备用选项,在正常情况下,任务完成后立即释放锁定。 您必须将其设置lockAtMostFor为比正常执行时间长得多的值。如果任务花费的时间超过 lockAtMostFor了所导致的行为,则可能无法预测(更多的进程将有效地持有该锁)。
    lockAtMostFor 单位 毫秒
    lockAtMostForString 使用“ PT14M” 意味着它将被锁定不超过14分钟。
  • lockAtLeastFor & lockAtLeastForString
    该属性指定应保留锁定的最短时间。其主要目的是在任务很短且节点之间的时钟差的情况下,防止从多个节点执行。
@Scheduled(fixedDelay = 1000*60*10)
@SchedulerLock(name = "queryRechargeBill", lockAtMostFor = 1000*60*15, lockAtLeastFor = 1000*60*5)
public void queryRechargeBill(){
    
    
    // do something
}

该锁将持有5分钟,5分钟释放,当节点异常或者死亡,该锁默认在15分钟后自动释放。在正常情况下,ShedLock在任务完成后立即释放锁定。实际上,我们不必这样做,因为@EnableSchedulerLock中提供了默认值, 但我们选择在此处覆盖它。

参考链接1:https://www.jianshu.com/p/94a0378798e1/
参考链接2:https://blog.csdn.net/wang20010104/article/details/127730997
参考链接3:https://blog.csdn.net/baidu_41634343/article/details/105660941

2、 基于Redis的分布式锁;

(1) 每个服务器生成随机uuid
(2) 利用redis的Setnx命令将随机uuid作为value存入redis
(3) 获取redis的value,与生成的随机uuid对比;
(4) value与生成的随机uuid匹配则执行,不匹配则不执行;
当前任务获取锁,如果获取到锁,则执行任务,如果获取不到,则什么都不干。

@Configuration
@Slf4j
public class ReportSchedule {
    
    

    @Autowired
    private RedissonClient redissonClient;

    @Value("${server.port}")
    private String port;

    private static final String LOCK = "lock";

    @Scheduled(cron = "*/5 * * * * ?")
    public void reportTask() throws Exception {
    
    
        RLock lock = redissonClient.getLock(LOCK);
        if (lock.tryLock()) {
    
    
            try {
    
    
                String hostAddress = InetAddress.getLocalHost().getHostAddress();
                log.info("ip:{},端口:{},在执行上报任务......", hostAddress, port);
                log.error("mark");
            } finally {
    
    
                lock.unlock();
            }
        }
    }
}

参考链接:https://blog.csdn.net/qq_34125999/article/details/126525626(有两种进阶改法)

猜你喜欢

转载自blog.csdn.net/m0_46459413/article/details/130106036