架构(十一)从0到1实现动态定时任务

一、引言

作者的平台项目最近需要实现一个功能,用户可选择这个任务什么时候执行,执行频率是什么?

这其实就是一个定时任务,只不过需要动态的,让用户自由选择。

二、原生实现

要实现这样的功能,可以直接依赖现有的中间件,比如作者就是使用qConfig+qSchedule实现的。但是在这之前,作者是想要原生的去实现,毕竟要调研很麻烦,另外各位读者使用框架不一定支持。

那么我们看看原生的需要怎么实现。首先需要定一下方案,一般的定时任务要么通过消息(mq、netty、http等)通知,要么直接客户端起一个线程不断轮训,随时通知需要执行的任务代码

既然做原生的不依赖任何外部,那就本地起个定时线程不断跑,看哪些任务需要跑了,把他们丢进线程池

1、首先用户可选的话,就要让用户可以填一个cron表达式,或者直接在配置文件里面加好各种选项,比如:1分钟一次、30分钟一次、一小时一次等等,展示给用户的是文字,配置文件里面文字是描述,实际上的key是cron表达式。

XML 复制代码
SCHEDULE_LIST=[{"code": "0 */15 * ? * *", "name": "每15分钟运行一次"},
{"code": "0 0 * ? * *", "name": "每小时运行一次"},
{"code": "0 0 0 ? * *", "name": "每天运行一次"},
{"code": "FALSE", "name": "不执行"}]

2、在任务表里面要有个字段,放这个cron表达式

3、启动一个定时任务

java 复制代码
@Scheduled(fixedRate = 1000*60) 
    public void schedule() {
        handle();
    }

4、在定时任务里面把需要执行的数据扔到线程池

这里需要注意,由于定时轮训的通知机制和处理速度,不管原生还是使用中间件,都是有可能导致一定误差的,这个误差可以做成配置,作者认为几万的数据量的话也就是前后三秒左右

所以这里还需要没有五秒以内的定时任务,正常也没有哪些任务需要那么高的频率,一般都是一分钟以上的

java 复制代码
 public void handle() {
// 查询所有任务数据
List<Task> tasks = queryTask();
for (Task task : tasks) {
    doHandle(task);
}
}

public void doHandle(Task task) {
        String cronExpression = task.getCron();
        Date now = new Date();
        CronExpression cron = new CronExpression(cronExpression);
        Date nextExecutionTime = cron.getNextValidTimeAfter(now);
        Date previousExecutionTime = cron.getPreviousValidTimeBefore(now);
        Date fiveSecondsBefore = new Date(now.getTime() - 5000);
        Date fiveSecondsAfter = new Date(now.getTime() + 5000);

        if ((nextExecutionTime.after(fiveSecondsBefore) && nextExecutionTime.before(fiveSecondsAfter))
                || (previousExecutionTime.after(fiveSecondsBefore) && previousExecutionTime.before(fiveSecondsAfter))) {
            doExecute(task);
        }
}

三、优化

这里也能看到上面还是有一些优化空间的,比如以下几点:

1、查询耗时

查数据库的任务再去判断是否执行,数量量不大还好,多了的话真是又占内存,要耗时间,可以做一层缓存,把任务的id和cron表达式存储在本地,然后由快速的做内存遍历,投入到线程池之后再去查明细

就是io会高一点,但是同一个时间执行的任务本身就不会很多,除非做的是集团那种规模的,定时任务几十万那种,基本没必要,因为能用中间件,各位读者自己就用了,直接看第四章好了

2、批量执行

判断这个任务的cron是否可以执行是非常快的,所以没必要一个个判断再投入线程池,完全可以20个一批投进去,根据执行情况调整批次数量

java 复制代码
List<List<Task>> batches = Lists.partition(tasks, batchSize);

for (List<Task> batch : batches) {
    doHandle(batch);
}

四、依赖框架

作者使用的是QSchedule和QConfig的组合,主要是把定时任务给放在配置中心之后,需要qschedule去拉取,然后生成对应的定时任务。

QSchedule是集团内部使用的定时任务,和集团的配置中心紧密结合,用起来很方便,配置好之后代码加个注解就行了,JobList.t就是配置中心文件的名字

java 复制代码
@QScheduleList("jobList.t")
    public void scheduleHandle(Parameter parameter) {
        handleSchedule(parameter.getJobName());
    }

不过QSchedule没开源,QConfig倒是开源了https://github.com/qunarcorp/qconfig

实现原理也不复杂,就是注解的切面拉取配置文件,再发给服务端生成定时任务,服务端每次通知客户端都会把名称、拓展信息相关的都带过来,根据名称再去数据库拉取要执行的任务。

减少了判断和多次的数据库io,而且定时任务有管理机制,可用性高。

五、总结

最小成本的快速实现需要根据自身环境,有成型的框架就直接用,没有自己写一个也不复杂。

相关推荐
ProtonBase10 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭9 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
工业甲酰苯胺9 小时前
分布式系统架构:服务容错
数据库·架构
Java程序之猿10 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
AskHarries11 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion12 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil712 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
小蜗牛慢慢爬行13 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾14 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全