在分布式的场景下,SpringBoot程序以集群的方式部署,这些程序中运行着相同的代码,如果其中有定时任务的话,所有的程序都会运行该任务,这样就会导致任务的重复执行。
由于所有的定时任务在集群的不同节点值中,所以需要一个专属的数据存储空间(通常使用Nosql数据库)来记录每一个定时任务的名称以及当前执行任务的主机与任务执行时间,而后在集群中不同的节点执行任务前会查看数据存储中是否存在指定任务的执行记录,如果没有记录则可以启动该节点任务,如果已经有此任务的相关信息,则代表任务已经执行,则跳过该节点任务。
简单来说,ShedLock可以保证定时任务在集群中只执行一次。
<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>
修改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回收一次空闲线程
ShedLock组件中有一个@SchedulerLock
注解用于定时任务方法上,该注解本质是启动了一个分布式独占锁,其内部有两个锁的配置项:
No. | 属性 | 解释 |
---|---|---|
1 | lockAtLeastFor | 成功执行定时任务时任务节点占有锁的最短时间 |
2 | lockAtMostFor | 成功执行定时任务时任务节点占有锁的最长时间 |
@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);
}
}
启动项目,定时任务生效,此时的任务执行间隔为6是因为配置了lockAtLeastFor = “5000”。
连接redis,发现自动存入了"job-lock:test:log-task"保存当前正在执行任务的主机信息。
以上程序虽然解决了集群下任务重复执行问题,但是依靠@Scheduled
注解配置cron表达式会将任务的执行时间固定,假如一个任务初始化时为每天10点执行,但是项目上线后觉得不妥,想要修改为每天12点执行,@Scheduled
注解的弊端就展露了出来。
@Scheduled(cron = "*/2 * * * * ?") // 2s执行一次
我们需要一种更加动态的方式设置任务执行时间。
新建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";
}
}
修改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
注解)。
启动项目,让其执行一段时间后调用接口:http://localhost:8080/cron/alter?cron=*/20 * * * * ?
观察程序执行结果:
一开始定时任务按默认设置的10s执行一次,调用接口传入cron后,定时任务20s执行一次。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/Nicholas_GUB/article/details/121572298
内容来源于网络,如有侵权,请联系作者删除!