最近有个小项目, 生产是要双活的, 硬件资源不足,整套微服务满足不了,因此阉割了只用 nacos 和 springboot ,在定时任务上要满足双活, 不能并发, 因此技术选型上就用了 Scheduled 与 SchedulerLock ;
先上执行结果:
2022-10-15 11:58:00.058 INFO 35600 --- [taskScheduler-2] c.p.c.c.q.service.impl.TaskServiceImpl : =============================== 我执行了 createFile ===============================
2022-10-15 11:58:00.058 INFO 35600 --- [taskScheduler-3] c.p.c.c.q.service.impl.TaskServiceImpl : =============================== 我执行了 getData ===============================
2022-10-15 11:58:30.059 INFO 35600 --- [taskScheduler-2] c.p.c.c.q.t.DataWarehouseScheduledUtil : I'm sleeped 30000ms end;
2022-10-15 11:58:45.059 INFO 35600 --- [taskScheduler-3] c.p.c.c.q.t.DataWarehouseScheduledUtil : I'm sleeped 45000ms end;
前提条件, SchedulerLock 的依赖jar版本号
<!-- 分布式锁依赖 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>3.0.0</version>
</dependency>
需新建的数据库表
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));
COMMENT ON TABLE shedlock IS '数据库锁';
COMMENT ON COLUMN shedlock.name IS '锁名称';
COMMENT ON COLUMN shedlock.lock_until IS '释放锁时间';
COMMENT ON COLUMN shedlock.locked_at IS '获取锁时间';
COMMENT ON COLUMN shedlock.locked_by IS '锁提供者';
我这里是用的oracle的,当然其他数据库的也可以,redis等都可以, 参考链接: ShedLock
启动类
@SpringBootApplication(scanBasePackages = Constants.SCAN_BASE_PACKAGES)
@EnableFeignClients
public class QueryApplication {
public static void main(String[] args) {
SpringApplication.run(QueryApplication.class, args);
}
}
分布式定时任务数据库锁及多线程配置
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.sql.DataSource;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import javax.sql.DataSource;
/**
* @author zengyujie
* @create 2022-10-15 10:13
* @Description
*/
@Configuration
@EnableScheduling
//defaultLockAtMostFor 指定在执行节点结束时应保留锁的默认时间使用ISO8601 Duration格式
//作用就是在被加锁的节点挂了时,无法释放锁,造成其他节点无法进行下一任务
//这里默认30s
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ScheduledLockConfig implements SchedulingConfigurer {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
}
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(64);
return taskScheduler;
}
}
定时任务组件:
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.core.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author zengyujie
* @create 2022-10-14 17:40
* @Description
*/
@Component
@Slf4j
public class DataWarehouseScheduledUtil {
@Resource
TaskService taskService;
/**
* cron 表示2分钟执行一次
* nacos配置中心方案 :@Scheduled(cron = "${sku.cron}")
*/
@Scheduled(cron = "0 0/2 * * * *")
@SchedulerLock(name = "executePushData",
lockAtMostFor = 1000*60*10,
lockAtLeastFor = 1000*60*10)
public void executePushData(){
taskService.createFile();
try {
Thread.sleep(30000);
log.info("I'm sleeped 30000ms end;");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 1000*60*10
* 表示间隔10分钟
*/
@Scheduled(cron = "0 0/2 * * * *") //这里建议配置在nacos的配置中心,统一管理
@SchedulerLock(name = "executeGetData",
lockAtMostFor = 1000*60*10, //这里建议配置在nacos的配置中心,统一管理
lockAtLeastFor = 1000*60*10)
public void executeGetData(){
taskService.getData();
try {
Thread.sleep(45000);
log.info("I'm sleeped 45000ms end;");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
做到这儿, 基本功能就已经实现了,但是如果我们想在nacos配置中心做定时任务的暂停处理呢? 修改定时任务的执行间隔呢?
配置中心增加配置 testScheduled.yaml
cron:
executeGetData: 0 0/2 * * * *
classPrefix: com.primeton.ckpt.namelist.task.TaskScheduledUtil.
enabled: false
增加配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* aop控制定时任务启停要用到它
* @author zengyujie
* @create 2023-06-14 20:37
* @Description
*/
@ConfigurationProperties("cron")
@Component
@Data
public class CronConfig {
//private String classPrefix;
private String enabled;
//它可以不用定义在配置类, 没必要
//private String executeGetData;
}
增加自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author zengyujie
* @create 2023-04-03 16:02
* @Description
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TaskScheduledAnnotation {
String methodName() default "";
}
我们稍微改造一下定时任务
/**
* 1000*60*10
* 表示间隔10分钟
*/
@Scheduled(cron = "${cron.executeGetData}") //使用配置中心配置
@SchedulerLock(name = "executeGetData",
lockAtMostFor = 1000*60*10, //这里建议配置在nacos的配置中心,统一管理
lockAtLeastFor = 1000*60*10)
@TaskScheduledAnnotation
public void executeGetData(){
taskService.getData();
try {
Thread.sleep(45000);
log.info("I'm sleeped 45000ms end;");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
aop
import com.primeton.ckpt.ca.police.annotation.TaskScheduledAnnotation;
import com.primeton.ckpt.ca.police.config.CronConfig;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 这儿如果用 @Value 获取 cron.enabled的值的话, 并且使用@RefreshScope刷新@Value 的值的话, 会看到日志打两次...
* @author zengyujie
* @create 2023-04-03 16:03
* @Description
*/
@Aspect
@Component
@Slf4j
public class ScheduledTaskProxy {
@Resource
CronConfig cronConfig;
@Around("@annotation(taskScheduledAnnotation)")
public void aroudTaskScheduledAnnotation(ProceedingJoinPoint point, TaskScheduledAnnotation taskScheduledAnnotation) throws Throwable{
// log.info("cron.enabled = {}",cronConfig.getEnabled());
long start = System.currentTimeMillis();
if ("true".equals(cronConfig.getEnabled())) {
log.info("调用定时任务方法:{}" , point.getSignature().getName());
//调用目标方法
point.proceed();
long end = System.currentTimeMillis();
log.info("调用定时任务方法 {} 结束: 耗时:{} ms", point.getSignature().getName(),end-start);
}
}
}
侦听配置文件刷新, 修改定时任务执行间隔
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* 动态定时器监听
* @author zengyujie
* @create 2023-04-06 09:00
* @Description
*/
@Component
@Slf4j
public class ScheduledTaskEnviromentChangeListener implements ApplicationListener<EnvironmentChangeEvent> {
@Resource
ApplicationContext applicationContext;
@Value("${cron.classPrefix}")
private String classPrefix ;
private ConcurrentHashMap<String, ScheduledFuture<?>> scheduledFutureConcurrentHashMap = new ConcurrentHashMap<>();
@Resource
private ConfigurableEnvironment configurableEnvironment;
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
for (String key: event.getKeys()){
log.info("key = {} 更新了", key );
if (key.startsWith("cron.")){
if ("cron.classPrefix".equals(key)){
classPrefix = configurableEnvironment.getProperty(key);
}else {
try {
ScheduledAnnotationBeanPostProcessor scheduledAnnotationBeanPostProcessor = applicationContext.getBean(ScheduledAnnotationBeanPostProcessor.class);
Field registrar = scheduledAnnotationBeanPostProcessor.getClass().getDeclaredField("registrar");
registrar.setAccessible(true);
ScheduledTaskRegistrar taskRegistrar = (ScheduledTaskRegistrar) registrar.get(scheduledAnnotationBeanPostProcessor);
TaskScheduler scheduler = taskRegistrar.getScheduler();
Set<ScheduledTask> scheduledTaskSet = scheduledAnnotationBeanPostProcessor.getScheduledTasks();
for (ScheduledTask scheduledTask:scheduledTaskSet){
log.info(scheduledTask.getTask().getRunnable().toString());
String methodName = key.split("\\.")[1];
String methodFullName = classPrefix+methodName;
if (scheduledTask.getTask().getRunnable().toString().equals(methodFullName)){
log.info("取消 {} 的执行......", methodFullName);
scheduledTask.cancel();
CronTask cronTask = new CronTask(scheduledTask.getTask().getRunnable(),configurableEnvironment.getProperty(key));
if (scheduledFutureConcurrentHashMap.containsKey(methodFullName)){
ScheduledFuture<?> scheduledFuture = scheduledFutureConcurrentHashMap.get(methodFullName);
scheduledFuture.cancel(true);
}
ScheduledFuture<?> scheduledFuture = scheduler.schedule(cronTask.getRunnable(),cronTask.getTrigger());
log.info("重置 {} 的执行 cron 为: {}", methodFullName, configurableEnvironment.getProperty(key));
scheduledFutureConcurrentHashMap.put(methodFullName,scheduledFuture);
//匹配到了就不再更新了。 退出 for (ScheduledTask scheduledTask:scheduledTaskSet){
break;
}
}
}catch (Exception e){
log.error("{}",e.getMessage());
}
}
}
}
}
}
最后的springboot使用nacos配置中心的我就不写了,这个参考nacos官方网站好了。
20230615更新: 添加配置中心控制定时任务间隔、 添加配置中心控制整体的定时任务是否执行。 对于为什么不做针对每个定时任务的启停控制, 是没这个需求。 单个的话可以参考其他的数据库动态定时任务的文章。