
思路分析
上篇文章重复请求的幂等接口设计的思考(二)提到使用定时任务扫描数据库中状态值为失败的业务定时再次调用接口。此时项目中未有现成的定时任务案例,如何处理?思路如下:
-
网络搜索springboot定时任务教程,直接采用注解方式(Spring Task)去实现。因为简单快速,在要求不高而追求速度的情况下采用,以实现功能为主。
-
后续随着定时任务增多,将其改成了异步注解的方式。
-
后面业务需求:将定时任务全部关闭。这个时候上述方式已无法满足需求,总不可能一行一行的注释代码吧。当时由于百度不到相关教程,能够实现灵活关闭定时任务的教程都是一些定时任务相关框架的使用,比如Quartz,XXL-JOB。这些方法当然能够实现需求(ps:复杂度提升,还要将原有的定时任务代码改造迁移,需要花费一定时间)。自己仔细琢磨着,由于我的需求只需要实现全部关闭定时任务,那么可以做一个开关放到配置文件中,这样只需要更改配置文件就行了,而由于spring task里面采用注解去实现的,那么顺着注解@EnableScheduling翻看源码,就能找到解决方法。
-
后期随着项目定时任务增多,业务需求也希望能够更好的管理配置定时任务,这个时候可以选择相关框架,推荐xxl-job,简单方便上手快,文档详细,功能也很全面。
springboot定时任务对接
在主类上使用@EnableScheduling注解开启对定时任务的支持 在定时任务的类或者方法上添加@Async实现异步 cron时间动态配置到配置文件中
@Component
@Async
public class SchedulingTest {
@Value("${paramInfo}")
private String paramInfo;
private static Logger log = LoggerFactory.getLogger(SchedulingTest.class);
@Scheduled(cron = "${cron.time}")
public void queryPayStatus() {
log.info("执行定时任务queryPayStatus---start");
log.info("执行定时任务queryPayStatus---end");
}
}
配置定时任务开关
新建SchedulerCondition类,配置定时任务开启条件 scheduling.enabled对应到配置文件中的值 scheduling.enabled=true 开启 scheduling.enabled=false 关闭
public class SchedulerCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
boolean enable = Boolean.valueOf(conditionContext.getEnvironment().getProperty("scheduling.enabled"));
return enable;
}
}
自定义启动类
@Configuration
public class Scheduler {
@Conditional(SchedulerCondition.class)
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
这样就完成了配置文件中scheduling.enabled的值来控制定时任务的启动与关闭。
自定义异步配置类
@Configuration
@EnableAsync
public class AsyncConfig {
@Value("${core.pool.size}")
private int corePoolSize;
@Value("${max.pool.size}")
private int maxPoolSize;
@Value("${queue.capacity}")
private int queueCapacity;
@Value("${keep.alive}")
private int keepAlive;
// 核心线程数(setCorePoolSize)10:线程池创建时候初始化的线程数
// 最大线程数(setMaxPoolSize)20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
// 缓冲队列(setQueueCapacity)200:用来缓冲执行任务的队列
// 允许线程的空闲时间(setKeepAliveSeconds)60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
// 线程池名的前缀(setThreadNamePrefix):设置好了之后可以方便我们定位处理任务所在的线程池
// 线程池对拒绝任务的处理策略(setRejectedExecutionHandler):这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在 execute
// 方法的调用线程中运行被拒绝的任务(setWaitForTasksToCompleteOnShutdown);如果执行程序已关闭,则会丢弃该任务
// setWaitForTasksToCompleteOnShutdown(true)该方法就是这里的关键,用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。
// 同时,这里还设置了setAwaitTerminationSeconds(60),该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAlive);
executor.setThreadNamePrefix("collect-");//自定义线程名称
// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
CRON表达式
每隔5秒执行一次:*/5 * * * * ?每隔1分钟执行一次:0 */1 * * * ?每天23点执行一次:0 0 23 * * ?每天凌晨1点执行一次:0 0 1 * * ?每月1号凌晨1点执行一次:0 0 1 1 * ?每月最后一天23点执行一次:0 0 23 L * ?每周星期天凌晨1点实行一次:0 0 1 ? * L在26分、29分、33分执行一次:0 26,29,33 * * * ?每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?“0 15 10 * * ? 2005” 2005年的每天上午10:15 触发
详细含义可以网上查找。
参考资料
更多文章
重复请求的幂等接口设计的思考(一) 三年程序员的第一篇公号文章
长按二维码关注,阅读我的程序员故事