架构(十一)从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,而且定时任务有管理机制,可用性高。

五、总结

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

相关推荐
阳光是sunny7 小时前
Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
前端·面试·架构
EMA13 小时前
Docker虚拟化失败解决方案
架构
李斯维13 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
Flynt15 小时前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
JouYY15 小时前
聊一下多 Agent 编排架构的应用实践
架构·llm·agent
Sunia16 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
ZhengEnCi2 天前
Q01-高并发点赞系统架构设计
架构
掉鱼的猫2 天前
Spring Boot → Solon 注解迁移实战指南:一张对照表说清楚
java·spring boot
笨鸟飞不快2 天前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
人活一口气2 天前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc