SpringBoot使用ShedLock调度任务

x33g5p2x  于2021-12-06 转载在 Spring  
字(4.7k)|赞(0)|评价(0)|浏览(347)

前言

在分布式的场景下,SpringBoot程序以集群的方式部署,这些程序中运行着相同的代码,如果其中有定时任务的话,所有的程序都会运行该任务,这样就会导致任务的重复执行。

由于所有的定时任务在集群的不同节点值中,所以需要一个专属的数据存储空间(通常使用Nosql数据库)来记录每一个定时任务的名称以及当前执行任务的主机与任务执行时间,而后在集群中不同的节点执行任务前会查看数据存储中是否存在指定任务的执行记录,如果没有记录则可以启动该节点任务,如果已经有此任务的相关信息,则代表任务已经执行,则跳过该节点任务。

简单来说,ShedLock可以保证定时任务在集群中只执行一次。

一、基础应用

1.1 引入依赖

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.29.0</version>
</dependency>

使用redis存储任务的调度记录,所以需要引入redis相关依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.0</version>
</dependency>

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
    <version>4.29.0</version>
</dependency>

引入连接池依赖。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

1.2 配置redis连接

修改application.yml文件,添加redis配置。

server:
  port: 8080
spring:
  profiles:
    active: test
  redis:
    host: 192.168.199.135 #redis主机地址
    port: 6379 # 端口
    password: 123456 #连接密码
    database: 0 # 数据库
    connect-timeout: 200
    timeout: 200
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 5
        max-wait: 1000
        time-between-eviction-runs: 2000 # 每2s回收一次空闲线程

1.3 编写定时任务

ShedLock组件中有一个@SchedulerLock注解用于定时任务方法上,该注解本质是启动了一个分布式独占锁,其内部有两个锁的配置项:

No.属性解释
1lockAtLeastFor成功执行定时任务时任务节点占有锁的最短时间
2lockAtMostFor成功执行定时任务时任务节点占有锁的最长时间
@Component
@Slf4j
public class ShedLockTask {

    private final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    @Scheduled(cron = "*/2 * * * * ?") // 2s执行一次
    @SchedulerLock(name = "log-task", lockAtLeastFor = "5000") // 5s后才能开启其他任务
    public void logTask() {
        log.info("【logTask】" + FORMAT.format(new Date()));
    }

}

编写@SpringEnv注解直接注入spring.profiles.active的内容。

import org.springframework.beans.factory.annotation.Value;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Value("${spring.profiles.active}")
public @interface SpringEnv {

}

编写配置类,设置存入redis中的数据。

@Configuration
@EnableScheduling // 启用定时任务
// 如果锁被占用且30s没有反应,则释放锁
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ShedLockRedisConfig {

    @SpringEnv
    private String env;

    @Bean
    public LockProvider getLockProvider(RedisConnectionFactory factory) {
        return new RedisLockProvider(factory, env);
    }
}

1.4 启动定时任务

启动项目,定时任务生效,此时的任务执行间隔为6是因为配置了lockAtLeastFor = “5000”。

连接redis,发现自动存入了"job-lock:test:log-task"保存当前正在执行任务的主机信息。

二、动态任务配置

以上程序虽然解决了集群下任务重复执行问题,但是依靠@Scheduled注解配置cron表达式会将任务的执行时间固定,假如一个任务初始化时为每天10点执行,但是项目上线后觉得不妥,想要修改为每天12点执行,@Scheduled注解的弊端就展露了出来。

@Scheduled(cron = "*/2 * * * * ?") // 2s执行一次

我们需要一种更加动态的方式设置任务执行时间。

2.1 外部传入cron 表达式

新建DynamicCronExpression 类,储存cron表达式(cron表达式一般会存储在sql数据库或者nosql数据库中,此处为了方便,将其存储在内存中)。

@Component
@Data
public class DynamicCronExpression {

    private String cron = "*/10 * * * * ?"; // 定义cron表达式
}

新建CronAction类,编写alter接口修改cron 表达式的内容。

@RestController
@RequestMapping("/cron/*")
@Slf4j
public class CronAction {
    private final DynamicCronExpression expression; // 注入表达式配置类

    @Autowired
    public CronAction(DynamicCronExpression expression) {
        this.expression = expression;
    }

    @GetMapping("/alter")
    public String alter(String cron) {
        log.info("修改cron表达式:{}", cron);
        expression.setCron(cron);
        return "success";
    }

}

2.2 为任务动态设置cron表达式。

修改ShedLockTask类,删除logTask方法上的@Scheduled注解。

@SchedulerLock(name = "log-task", lockAtLeastFor = "5000")
public void logTask() {
    log.info("【logTask】" + FORMAT.format(new Date()));
}

新建ScheduleConfig类,实现SchedulingConfigurer接口(负责配置任务和执行时间)。

@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer { // 动态配置

    private DynamicCronExpression expression; // 注入表达式配置类
    private ShedLockTask shedLockTask; // 注入任务

    @Autowired
    public ScheduleConfig(DynamicCronExpression expression, ShedLockTask shedLockTask) {
        this.expression = expression;
        this.shedLockTask = shedLockTask;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
            () -> shedLockTask.logTask(), // 设置要执行的任务
            triggerContext -> {
                log.info("当前cron表达式: {}", expression.getCron());
                return new CronTrigger((expression.getCron())).nextExecutionTime(triggerContext); // 设置cron表达式
            }
        );
    }

}

ScheduledTaskRegistrar 类中存在一个 addTriggerTask 方法,参数接收 Runnable 接口和 Trigger 接口,Runnable用来设置要执行的任务Trigger接口用来设置执行时间(此处刚好取代了@Scheduled注解)。

2.3 观察执行

启动项目,让其执行一段时间后调用接口:http://localhost:8080/cron/alter?cron=*/20 * * * * ?

观察程序执行结果:

一开始定时任务按默认设置的10s执行一次,调用接口传入cron后,定时任务20s执行一次。

相关文章