打造卓越任务调度体系:实用攻略与技巧解析

 写这篇文章,想和大家从头到脚说说任务调度,希望大家读完之后,能够理解实现一个任务调度系统的核心逻辑。

1 Quartz

Quartz 是一款 Java 开源任务调度框架,也是很多 Java 工程师接触任务调度的起点。

下图显示了任务调度的整体流程: 

Quartz 的核心是三个组件。

  • 任务:Job 用于表示被调度的任务;
  • 触发器:Trigger 定义调度时间的元素,即按照什么时间规则去执行任务。一个 Job 可以被多个 Trigger 关联,但是一个 Trigger 只能关联一个 Job;
  • 调度器 :工厂类创建 Scheduler,根据触发器定义的时间规则调度任务。

上图代码中 Quartz 的 JobStore 是 RAMJobStore,Trigger 和 Job 存储在内存中。

执行任务调度的核心类是 QuartzSchedulerThread 。

  1. 调度线程从 JobStore 中获取需要执行的的触发器列表,并修改触发器的状态;
  2. <font color="red">Fire</font> 触发器,修改触发器信息 (下次执行触发器的时间,以及触发器状态),并存储起来。
  3. 最后创建具体的执行任务对象,通过 worker 线程池执行任务。

接下来再聊聊 Quartz 的集群部署方案。

Quartz 的集群部署方案,需要针对不同的数据库类型 (MySQL , ORACLE) 在数据库实例上创建 Quartz 表,JobStore 是: JobStoreSupport 。

这种方案是分布式的,没有负责集中管理的节点,而是利用数据库行级锁的方式来实现集群环境下的并发控制。

scheduler 实例在集群模式下首先获取 {0} LOCKS 表中的行锁,Mysql 获取行锁的语句:

{0} 会替换为配置文件默认配置的 QRTZ_。sched_name 为应用集群的实例名,lock_name 就是行级锁名。Quartz 主要有两个行级锁触发器访问锁 (TRIGGER_ACCESS) 和 状态访问锁(STATE_ACCESS)。

这个架构解决了任务的分布式调度问题,同一个任务只能有一个节点运行,其他节点将不执行任务,当碰到大量短任务时,各个节点频繁的竞争数据库锁,节点越多性能就会越差。

2 分布式锁模式

Quartz 的集群模式可以水平扩展,也可以分布式调度,但需要业务方在数据库中添加对应的表,有一定的强侵入性。

有不少研发同学为了避免这种侵入性,也探索出分布式锁模式

业务场景:电商项目,用户下单后一段时间没有付款,系统就会在超时后关闭该订单。

通常我们会做一个定时任务每两分钟来检查前半小时的订单,将没有付款的订单列表查询出来,然后对订单中的商品进行库存的恢复,然后将该订单设置为无效。

我们使用 Spring Schedule 的方式做一个定时任务。

@Scheduled(cron = "0 */2 * * * ? ")
public void doTask() {log.info("定时任务启动");//执行关闭订单的操作orderService.closeExpireUnpayOrders();log.info("定时任务结束");}

在单服务器运行正常,考虑到高可用,业务量激增,架构会演进成集群模式,在同一时刻有多个服务执行一个定时任务,有可能会导致业务紊乱。

解决方案是在任务执行的时候,使用 Redis 分布式锁来解决这类问题。

@Scheduled(cron = "0 */2 * * * ? ")
public void doTask() {log.info("定时任务启动");String lockName = "closeExpireUnpayOrdersLock";RedisLock redisLock = redisClient.getLock(lockName);//尝试加锁,最多等待3秒,上锁以后5分钟自动解锁boolean locked = redisLock.tryLock(3, 300, TimeUnit.SECONDS);if(!locked){log.info("没有获得分布式锁:{}" , lockName);return;}try{//执行关闭订单的操作orderService.closeExpireUnpayOrders();} finally {redisLock.unlock();}log.info("定时任务结束");
}

Redis 的读写性能极好,分布式锁也比 Quartz 数据库行级锁更轻量级。当然 Redis 锁也可以替换成 Zookeeper 锁,也是同样的机制。

在小型项目中,使用:定时任务框架(Quartz/Spring Schedule)和 分布式锁(redis/zookeeper)有不错的效果。

但是呢?我们可以发现这种组合有两个问题:

  1. 定时任务在分布式场景下有空跑的情况,而且任务也无法做到分片;
  2. 要想手工触发任务,必须添加额外的代码才能完成。

3 ElasticJob-Lite 框架

ElasticJob-Lite 定位为轻量级无中心化解决方案,使用 jar 的形式提供分布式任务的协调服务。 

官网架构图

应用内部定义任务类,实现 SimpleJob 接口,编写自己任务的实际业务流程即可。

public class MyElasticJob implements SimpleJob {@Overridepublic void execute(ShardingContext context) {switch (context.getShardingItem()) {case 0:// do something by sharding item 0break;case 1:// do something by sharding item 1break;case 2:// do something by sharding item 2break;// case n: ...}}
}

举例:应用 A 有五个任务需要执行,分别是 A,B,C,D,E。任务 E 需要分成四个子任务,应用部署在两台机器上。

应用 A 在启动后, 5 个任务通过 Zookeeper 协调后被分配到两台机器上,通过 Quartz Scheduler 分开执行不同的任务。

ElasticJob 从本质上来讲 ,底层任务调度还是通过 Quartz ,相比 Redis 分布式锁 或者 Quartz 分布式部署 ,它的优势在于可以依赖 Zookeeper 这个大杀器 ,将任务通过负载均衡算法分配给应用内的 Quartz Scheduler 容器。

从使用者的角度来讲,是非常简单易用的。但从架构来看,调度器和执行器依然在同一个应用方 JVM 内,而且容器在启动后,依然需要做负载均衡。应用假如频繁的重启,不断的去选主,对分片做负载均衡,这些都是相对比较的操作。

另外,ElasticJob 的控制台是比较粗糙的,通过读取注册中心数据展现作业状态,更新注册中心数据修改全局任务配置。

4 中心化流派

中心化的原理是:把调度和任务执行,隔离成两个部分:调度中心和执行器。调度中心模块只需要负责任务调度属性,触发调度命令。执行器接收调度命令,去执行具体的业务逻辑,而且两者都可以进行分布式扩容。

4.1 MQ 模式

先谈谈我在艺龙促销团队接触的第一种中心化架构。

调度中心依赖 Quartz 集群模式,当任务调度时候,发送消息到 RabbitMQ 。业务应用收到任务消息后,消费任务信息。

这种模型充分利用了 MQ 解耦的特性,调度中心发送任务,应用方作为执行器的角色,接收任务并执行。

但这种设计强依赖消息队列,可扩展性和功能,系统负载都和消息队列有极大的关联。这种架构设计需要架构师对消息队列非常熟悉。

4.2 XXL-JOB

XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

xxl-job 2.3.0架构图

我们重点剖析下架构图 :

▍ 网络通讯 server-worker 模型

调度中心和执行器 两个模块之间通讯是 server-worker 模式。调度中心本身就是一个 SpringBoot 工程,启动会监听 8080 端口。

执行器启动后,会启动内置服务( EmbedServer )监听 9994 端口。这样双方都可以给对方发送命令。

那调度中心如何知道执行器的地址信息呢 ?上图中,执行器会定时发送注册命令 ,这样调度中心就可以获取在线的执行器列表。

通过执行器列表,就可以根据任务配置的路由策略选择节点执行任务。常见的路由策略有如下三种:

  • 随机节点执行:选择集群中一个可用的执行节点执行调度任务。适用场景:离线订单结算。

  • 广播执行:在集群中所有的执行节点分发调度任务并执行。适用场景:批量更新应用本地缓存。

  • 分片执行:按照用户自定义分片逻辑进行拆分,分发到集群中不同节点并行执行,提升资源利用效率。适用场景:海量日志统计。

▍ 调度器

调度器是任务调度系统里面非常核心的组件。XXL-JOB 的早期版本是依赖 Quartz。

但在 v2.1.0 版本中完全去掉了 Quartz 的依赖,原来需要创建的 Quartz 表也替换成了自研的表。

核心的调度类是:JobTriggerPoolHelper 。调用 start 方法后,会启动两个线程:scheduleThread 和 ringThread 。

首先 scheduleThread 会定时从数据库加载需要调度的任务,这里从本质上还是基于数据库行锁保证同时只有一个调度中心节点触发任务调度。

Connection conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
preparedStatement = conn.prepareStatement(
"select * from xxl_job_lock where lock_name = 'schedule_lock' for update");
preparedStatement.execute();
# 触发任务调度 (伪代码)
for (XxlJobInfo jobInfo: scheduleList) {// 省略代码
}
# 事务提交
conn.commit();

调度线程会根据任务的「下次触发时间」,采取不同的动作:

已过期的任务需要立刻执行的,直接放入线程池中触发执行 ,五秒内需要执行的任务放到 ringData 对象里。

ringThread 启动后,定时从 ringData 对象里获取需要执行的任务列表 ,放入到线程池中触发执行。

5 自研在巨人的肩膀上

2018 年,我有一段自研任务调度系统的经历。

背景是:兼容技术团队自研的 RPC 框架,技术团队不需要修改代码,RPC 注解方法可以托管在任务调度系统中,直接当做一个任务来执行。

自研过程中,研读了 XXL-JOB 源码,同时从阿里云分布式任务调度 SchedulerX 吸取了很多营养。

SchedulerX 1.0 架构图

  • Scheduler-console 是任务调度的控制台,用于创建、管理定时任务。负责数据的创建、修改和查询。在产品内部与 scheduler server 交互。
  • Scheduler-server 是任务调度的服务端,是 Scheduler 的核心组件。负责客户端任务的调度触发以及任务执行状态的监测。
  • Scheduler-client 是任务调度的客户端。每个接入客户端的应用进程就是一个的 Worker。 Worker 负责与 scheduler-server 建立通信,让 scheduler-server 发现客户端的机器。 并向 scheduler-server 注册当前应用所在的分组,这样 scheduler-server 才能向客户端定时触发任务。

我们模仿了 SchedulerX 的模块,架构设计如下图:

我选择了 RocketMQ 源码的通讯模块 remoting 作为自研调度系统的通讯框架。基于如下两点:

  1. 我对业界大名鼎鼎的 Dubbo 不熟悉,而 remoting 我已经做了多个轮子,我相信自己可以搞定;

  2. 在阅读 SchedulerX 1.0 client 源码中,发现 SchedulerX 的通讯框架和 RocketMQ Remoting 很多地方都很类似。它的源码里有现成的工程实现,完全就是一个宝藏。

我将 RocketMQ remoting 模块去掉名字服务代码,做了一定程度的定制。

在 RocketMQ 的 remoting 里,服务端采用 Processor 模式。

调度中心需要注册两个处理器:回调结果处理器 CallBackProcessor 和心跳处理器 HeartBeatProcessor 。执行器需要注册触发任务处理器 TriggerTaskProcessor 。

public void registerProcessor(int requestCode,NettyRequestProcessor processor,ExecutorService executor);

处理器的接口:

public interface NettyRequestProcessor {RemotingCommand processRequest(ChannelHandlerContext ctx,RemotingCommand request) throws Exception;boolean rejectRequest();
}

对于通讯框架来讲,我并不需要关注通讯细节,只需要实现处理器接口即可。

以触发任务处理器 TriggerTaskProcessor 举例:

搞定网络通讯后,调度器如何设计 ?最终我还是选择了 Quartz 集群模式。主要是基于以下几点原因:

  1. 调度量不大的情况下 ,Quartz 集群模式足够稳定,而且可以兼容原来的 XXL-JOB 任务;
  2. 使用时间轮的话,本身没有足够的实践经验,担心出问题。 另外,如何让任务通过不同的调度服务(schedule-server)触发, 需要有一个协调器。于是想到 Zookeeper。但这样的话,又引入了新的组件。
  3. 研发周期不能太长,想快点出成果。

自研版的调度服务花费一个半月上线了。系统运行非常稳定,研发团队接入也很顺畅。 调度量也不大 ,四个月总共接近 4000 万到 5000 万之间的调度量。

坦率的讲,自研版的瓶颈,我的脑海里经常能看到。 数据量大,我可以搞定分库分表,但 Quartz 集群基于行级锁的模式 ,注定上限不会太高。

为了解除心中的困惑,我写一个轮子 DEMO 看看可否 work:

  1. 去掉外置的注册中心,调度服务(schedule-server)管理会话;
  2. 引入 zookeeper,通过 zk 协调调度服务。但是 HA 机制很粗糙,相当于一个任务调度服务运行,另一个服务 standby;
  3. Quartz 替换成时间轮 (参考 Dubbo 里的时间轮源码)。

这个 Demo 版本在开发环境可以运行,但有很多细节需要优化,仅仅是个玩具,并没有机会运行到生产环境。

最近读阿里云的一篇文章《如何通过任务调度实现百万规则报警》,SchedulerX2.0 高可用架构见下图:

文章提到:

每个应用都会做三备份,通过 zk 抢锁,一主两备,如果某台 Server 挂了,会进行 failover,由其他 Server 接管调度任务。

这次自研任务调度系统从架构来讲,并不复杂,实现了 XXL-JOB 的核心功能,也兼容了技术团队的 RPC 框架,但并没有实现工作流以及 mapreduce 分片。

SchedulerX 在升级到 2.0 之后基于全新的 Akka 架构,这种架构号称实现高性能工作流引擎,实现进程间通信,减少网络通讯代码。

在我调研的开源任务调度系统中,PowerJob 也是基于 Akka 架构,同时也实现了工作流和 MapReduce 执行模式。

我对 PowerJob 非常感兴趣,也会在学习实践后输出相关文章,敬请期待。

6 技术选型

首先我们将任务调度开源产品和商业产品 SchedulerX 放在一起,生成一张对照表:

Quartz 和 ElasticJob 从本质上还是属于框架的层面。

中心化产品从架构上来讲更加清晰,调度层面更灵活,可以支持更复杂的调度(mapreduce 动态分片,工作流)。

XXL-JOB 从产品层面已经做到极简,开箱即用,调度模式可以满足大部分研发团队的需求。简单易用 + 能打,所以非常受大家欢迎。

其实每个技术团队的技术储备不尽相同,面对的场景也不一样,所以技术选型并不能一概而论。

不管是使用哪种技术,在编写任务业务代码时,还是需要注意两点:

  • 幂等。当任务被重复执行的时候,或者分布式锁失效的时候,程序依然可以输出正确的结果;
  • 任务不跑了,千万别惊慌。查看调度日志,JVM 层面使用 Jstack 命令查看堆栈,网络通讯要添加超时时间 ,一般能解决大部分问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/22434.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于STC12C5A60S2系列1T 8051单片机实现一主单片机与一从单片机相互发送数据的RS485通信功能

基于STC12C5A60S2系列1T 8051单片机实现一主单片机与一从单片机相互发送数据的RS485通信功能的RS485通信功能 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机串口通信介绍STC12C5A60S2系列1T 8051单片机串口通信的结构基于STC12C5A60S2系列1T 8051单片机串…

zdppy_api 中间件请求原理详解

单个中间件的逻辑 整体执行流程&#xff1a; 1、客户端发起请求2、中间件拦截请求&#xff0c;在请求开始之前执行业务逻辑3、API服务接收到中间件处理之后的请求&#xff0c;和数据库交互&#xff0c;请求数据4、数据库返回数据5、API处理数据库的数据&#xff0c;然后给客户…

【第十一课】空间数据基础与处理——属性数据管理

一、前言 Arcgis分析离不开两大主体数据&#xff0c;一是空间&#xff0c;二是经济属性。在运用 Aecgis 进行分析时&#xff0c;经常会碰到一些涉及多要素的属性更改或填写&#xff0c; 如果按照普通的方法&#xff0c;每个属性进行修改或填写的话&#xff0c;工作量是很大的&…

Apache OFBiz 路径遍历导致RCE漏洞复现(CVE-2024-36104)

0x01 产品简介 Apache OFBiz是一个电子商务平台,用于构建大中型企业级、跨平台、跨数据库、跨应用服务器的多层、分布式电子商务类应用系统。是美国阿帕奇(Apache)基金会的一套企业资源计划(ERP)系统。该系统提供了一整套基于Java的Web应用程序组件和工具。 0x02 漏洞概…

【深入理解计算机系统第3版】补码加法

感觉这部分有点难&#xff0c;所以稍微整理记一下。 抱歉中英混合&#xff0c;来回切换输入法真的很折磨人。 负溢出 正常 正溢出 以4位补码加法为例&#xff0c;理解下表(书中P64) 补码最大值Tmax 2^3 - 1 7, 补码最小值Tmin -2^3 -8 xyz x yz z mod 2^4zU2Tw(z)溢…

超燃混剪热门视频素材去哪里找? 爆款超燃网站合集分享

在今天的数字时代&#xff0c;短视频已成为传播信息和个人表达的主流方式。无论你是混剪爱好者还是自媒体创作者&#xff0c;掌握如何获取和利用高质量的视频素材是关键。本文将介绍几个顶级的视频素材网站&#xff0c;包括国内外的平台&#xff0c;帮助你创建引人入胜的视频作…

【数据结构】二叉搜索树--BST,Binary Search Tree

文章目录 二叉搜索树1. 二叉搜索树的概念2. 二叉搜索树的接口2.1 查找非递归查找递归查找 2.2 中序遍历2.3 插入非递归插入递归插入 2.4 删除非递归删除递归删除 3. 二叉搜索树的应用key搜索模型kv搜索模型 5. oj题 二叉搜索树 1. 二叉搜索树的概念 二叉搜索树又称二叉排序树…

12-学生们参加各科测试的次数(高频 SQL 50 题基础版)

12-学生们参加各科测试的次数 -- 学生表中&#xff0c;id是唯一的&#xff0c;将他作为主表 -- CROSS JOIN产生了一个结果集&#xff0c;该结果集是两个关联表的行的乘积 -- 2行表,与3行表使用cross join,得到2*36行数据 select st.student_id, st.student_name,su.subject_na…

Vxe UI vxe-upload vue上传组件,显示进度条的方法

vxe-upload vue 上传组件 查看官网 https://vxeui.com 显示进度条很简单&#xff0c;需要后台支持进度就可以了&#xff0c;后台实现逻辑具体可以百度&#xff0c;这里只介绍前端逻辑。 vue 上传附件 相关参数说明&#xff0c;具体可以看文档&#xff1a; multiple 是否允许…

现代密码学-国密算法

商用密码算法种类 商用密码算法 密码学概念、协议与算法之间的依赖关系 数字签名、证书-公钥密码、散列类算法 消息验证码-对称密码 &#xff0c;散列类 安全目标与算法之间的关系 机密性--对称密码、公钥密码 完整性--散列类算法 可用性--散列类、公钥密码 真实性--公…

项目雅景临居---模块2 标签管理

一、 [根据类型]查询标签列表 Autowiredprivate LabelInfoService service;Operation(summary "&#xff08;根据类型&#xff09;查询标签列表")GetMapping("list")public Result<List<LabelInfo>> labelList(RequestParam(required false)…

Linux学习笔记6 进程角度看内存泄露

一&#xff0c;从进程角度看堆区内存申请与释放问题 1&#xff0c;c语言中的内存泄漏 内存溢出&#xff1a;申请内存时&#xff0c;没用足够的内存可以使用。 内存泄露&#xff1a;严格来说&#xff0c;只有对象不会再被程序用到了&#xff0c;但是GC又不能回收它们的情况&…

ue项目更改项目缓存 防止跑ue项目c盘占用过多内存

原本项目缓存 在ue引擎所在的文件夹里找好baseEnging.ini 搜索install 找到第7个install 更改缓存路径到指定位置

【Text2SQL 论文】QDecomp:探索 CoT-style 的 prompt 来解决 Text2SQL

论文&#xff1a;Exploring Chain of Thought Style Prompting for Text-to-SQL ⭐⭐⭐⭐ EMNLP 2023, arXiv:2305.14215 一、论文速读 本文通过对 LLM 使用 CoT-style 的 prompting 方法来解决 Text2SQL 问题&#xff0c;试图回答下面两个问题&#xff1a; 哪种 prompting s…

性能工具之 JMeter 常用组件介绍(二)

文章目录 一、Thread Group二、断言组件1、Response Assertion&#xff1a;响应断言2、Response Assertion&#xff1a;响应断言3、Duration Assertion&#xff1a;响应时间断言4.、JSON Assertion&#xff1a;json断言 一、Thread Group 线程组也叫用户组&#xff0c;是性能测…

通过血清拉曼光谱进行COVID-19的高效初步筛查

通过血清拉曼光谱进行COVID-19的高效初步筛查 原创 小王搬运工 时序课堂 2024-06-04 20:04 四川 论文地址&#xff1a;https://analyticalsciencejournals.onlinelibrary.wiley.com/doi/full/10.1002/jrs.6080 论文源码&#xff1a;无 期刊&#xff1a;JOURNAL OF RAMAN SPE…

LangChain学习之 Question And Answer的操作

1. 学习背景 在LangChain for LLM应用程序开发中课程中&#xff0c;学习了LangChain框架扩展应用程序开发中语言模型的用例和功能的基本技能&#xff0c;遂做整理为后面的应用做准备。视频地址&#xff1a;基于LangChain的大语言模型应用开发构建和评估。 2. Q&A的作用 …

Echarts 中type是value的X轴在设置了interval间隔后没有展示

文章目录 问题分析问题 Echarts中type是value的X轴在设置了interval间隔后没有展示 分析 之前代码是这样写的:axisLabel 属性中设置了 interval ,但未起作用,原因如下 在 ECharts 中,interval 属性是用于类目型(category)轴的刻度间隔设置,并不适用于数值型(value)…

解决 clickhouse jdbc 偶现 failed to respond 问题

背景 Clickhouse集群版本为 Github Clickhouse 22.3.5.5&#xff0c; clickhouse-jdbc 版本为 0.2.4。 问题表现 随着业务需求的扩展&#xff0c;基于Clickhouse 需要支持更多任务在期望的时效内完成&#xff0c;于是将业务系统和Clickhouse交互的部分都提交给可动态调整核心…

windows上安装MongoDB,springboot整合MongoDB

上一篇文章已经通过在Ubuntu上安装MongoDB详细介绍了MongoDB的各种命令用法。 Ubuntu上安装、使用MongoDB详细教程https://blog.csdn.net/heyl163_/article/details/133781878 这篇文章介绍一下在windows上安装MongoDB&#xff0c;并通过在springboot项目中使用MongoDB记录用户…