Cloud Native
定时任务是业务应用开发中非常普遍存在的场景(如:每分钟扫描超时支付的订单,每小时清理一次数据库历史数据,每天统计前一天的数据并生成报表等等), 解决方案很多 ,Spring 框架提供了一种通过注解来配置定时任务的解决方案,接入非常的简单,仅需如下两步:
@SpringBootApplication
@EnableScheduling // 添加定时任务启动注解
public class SpringSchedulerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSchedulerApplication.class, args);
}
}
复制代码
@Component
public class SpringScheduledProcessor {
/**
* 通过Cron表达式指定频率或指定时间
*/
@Scheduled(cron = "0/5 * * * * ?")
public void doSomethingByCron() {
System.out.println("do something");
}
/**
* 固定执行间隔时间
*/
@Scheduled(fixedDelay = 2000)
public void doSomethingByFixedDelay() {
System.out.println("do something");
}
/**
* 固定执行触发频率
*/
@Scheduled(fixedRate = 2000)
public void doSomethingByFixedRate() {
System.out.println("do something");
}
}
复制代码
Spring 定时任务原理
Cloud Native
Spring 定时任务核心逻辑主要在 spring-context 中的 scheduling 包中,其主要结构包括:
业务逻辑会将被包装在 ScheduledMethodRunnable 类中,其中包含了待执行的目标业务对象 Bean 和业务方法,该 Runnable 对象在运行时会被提交至 ScheduledExecutorService 调度线程池完成任务的定时运行。
从上图可以看到真正要运行的业务逻辑 ScheduledMethodRunnable 会被 ReschedulingRunnable、DelegatingErrorHandlingRunnable 做了代理扩展,这两层代理扩展具有如下意义:
Spring 定时任务 Task 类的模式主要可分为两类:IntervalTask 和 TriggerTask。前者表示固定频率间隔执行,后者则采用 Trigger 触发器模式实现定时调度,Cron 表达式配置为该模式实现。
默认配置下底层运行的线程池为单线程,单线程的运行模型在任务量较多且触发频率较高的情况下,一旦某个任务发生阻塞会导致所有后续定时任务运行阻断,这对业务运行带来严重隐患。常见可采用如下方式:
@Scheduled(fixedDelay = 2000)
@Async
public void test() {
System.out.println(DateUtil.now()+ " test.");
}
复制代码
定时任务运行可设置统一异常处理,基于 ErrorHandler 接口开发对应异常处理实现类。对应的异常实现处理类需要注入到核心的 ThreadPoolTaskScheduler 中,用户可以通过自定义 TaskSchedulerCustomizer 方式来实现 ErrorHandler 自定义异常处理 Bean 注入至 ThreadPoolTaskScheduler 中。
@Component
public class DemoTaskSchedulerCustomizer implements TaskSchedulerCustomizer {
@Override
public void customize(ThreadPoolTaskScheduler taskScheduler) {
taskScheduler.setErrorHandler(new DemoErrorHandler());
}
private class DemoErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable throwable) {
System.out.println("异常统一处理.");
}
}
}
复制代码
Cloud Native
Spring 定时任务,只要有注解就会执行,在分布式场景下,所有机器代码一致,会导致同一个任务在多台机器上重复执行。 一般的解决方案是抢锁触发,分布式锁实现形式可采用 DB、ZK、Redis 等方式。
示例代码 如下:
@Component
@EnableScheduling
public class MyTask {
/**
* 每分钟的第30秒跑一次
*/
@Scheduled(cron = "30 * * * * ?")
public void task1() throws Exception {
String lockName = "task1";
if (tryLock(lockName)) {
System.out.println("hello cron");
releaseLock(lockName);
} else {
return;
}
}
private boolean tryLock(String lockName) {
//TODO
return true;
}
private void releaseLock(String lockName) {
//TODO
}
}
复制代码
如 上图所示,当任务触发时 3 个 server 会对任务抢锁,仅获得任务锁的 server 才能执行对应任务业务逻辑。 当前的这个设计,仔细一点的同学可以发现,其实还是有可能导致任务重复执行的。 比如任务执行的非常快,A 这台机器抢到锁,执行完任务后很快就释放锁了。 B 这台机器后抢锁,还是会抢到锁,再执行一遍任务。
原生 Spring 定时任务没有控制台,无法动态的新增和修改定时任务,如果要修改定时任务的配置(比如每分钟跑一次改成每小时跑一次),必须修改代码重新发布应用。 同时原生Spring定时任务也没有运维操作,不支持运行一次任务,任务失败了也不支持重跑任务。
如果要自研的可视化控制台来实现整套任务可视化管控体系,需要一定的前后端研发成本和服务部署成本投入。对于需要自建的用户而言,可参考以下需求功能进行自有平台建设:
对于完整企业级定时任务运用方案中,报警通知能力必不可少,任务跑失败了需要及时通知到用户,否则可能产生故障。
原生 Spring 定时任务不支持报警通知能力,如果要自研,可以参考上一章节中《异常统一处理》对任务失败的信息进行收集,构建相应的异常处理机制(包括对接各类报警平台进行异常消息通知处理,定义异常等级和类别进行不同的通知策略),然后进行 定时任务报警通知。
定时任务在运行过程中会存在各种各样的问题,比如: 执行失败、执行耗时、执行卡住等,这些都需要在后期实际运维去定位快速分析。 在对应分析过程中没有高效在线排查能力的话将遇到很多棘手的问题:
阿里云 Spring 定时任务企业级解决方案
Cloud Native
接下来主要讲下如何利用公有云上任务调度 SchedulerX 轻松接入基于 Spring 开发的定时任务。前面聊了基于 Spring 原生功能在使用过程中面临的问题及需要自行处理解决的相关方案,可以看到仅针对企业级最基础的运用场景下就需要花费较多的改造投入及相关服务后续运维投入。通过接入 SchedulerX 任务调度平台,原本 Spring 定时任务使用者可无缝且 0 改造获得企业级运用所需能力,同时降低了自研部署运维定时服务相关组件的技术成本。
对于 SchedulerX 新用户而言接入仅需三步(参考附件接入手册):
# 配置表示由SchedulerX接管Spring定时任务运行
spring.schedulerx2.task.scheduling.scheduler=schedulerx
复制代码
# 自动同步Spring定时任务至调度平台,无需单独手动创建(默认不开启)
spring.schedulerx2.task.scheduling.sync=true
复制代码
提供白屏控制台可以动态新增、修改、启用、禁用任务,支持运行一次、原地重跑、重刷数据、停止任务、标记成功等运维操作。
支持执行记录查看、执行业务日志查询、执行全链路追踪。
SchedulerX 提供丰富的报警通知能力 ,支持短信、电话、邮件、webhook 报警,支持报警联系人组和报警历史,可白屏动态配置。
总结
Cloud Native
本文主要从 Spring 定时任务的运行机制进行剖析阐述,并对如何扩展框架原生能力以满足企业级生产环境运行定时任务所需各种场景提出了相应的建议,用户可作参考构建自己内部定时任务方案。 同时就阿里云上提供的任务调度服务如何接入 Spring 定时任务的运行进行讲解,并简单展示了接入后所带来的企业级能力。 最后欢迎有定时任务业务需求用户可先通过基础免费额度体验感受云上服务带来便捷。