如何实现一个任务调度系统(一)

iamwaiwai
发布于 2022-5-17 17:46
浏览
0收藏

阅读一篇「定时任务框架选型」的文章时,一位网友的留言到了我:

 

我看过那么多所谓的教程,大部分都是教“如何使用工具”的,没有多少是教“如何制作工具”的,能教“如何仿制工具”的都已经是凤毛麟角,中国 软件行业,缺的是真正可以“制作工具”的程序员,而绝对不缺那些“使用工具”的程序员!......  ”这个业界最不需要的就是“会使用XX工具的工程师”,而是“有创造力的软件工程师”!业界所有的饭碗,本质就是“有创造力的软件工程师”提供出来的啊!

 

写这篇文章,想和大家从头到脚说说任务调度,希望大家读完之后,能够理解实现一个任务调度系统的核心逻辑。如何实现一个任务调度系统(一)-鸿蒙开发者社区1 Quartz


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

 

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

如何实现一个任务调度系统(一)-鸿蒙开发者社区

Quartz的核心是三个组件。

 

  ◆  任务:Job 用于表示被调度的任务;
 

◆  触发器:Trigger 定义调度时间的元素,即按照什么时间规则去执行任务。一个Job可以被多个Trigger关联,但是一个Trigger 只能关联一个Job;
 

◆  调度器 :工厂类创建Scheduler,根据触发器定义的时间规则调度任务。
如何实现一个任务调度系统(一)-鸿蒙开发者社区上图代码中Quartz 的JobStore是 RAMJobStore,Trigger 和 Job 存储在内存中。

 

执行任务调度的核心类是 QuartzSchedulerThread 如何实现一个任务调度系统(一)-鸿蒙开发者社区1.调度线程从JobStore中获取需要执行的的触发器列表,并修改触发器的状态;


2.Fire触发器,修改触发器信息(下次执行触发器的时间,以及触发器状态),并存储起来。


最后创建具体的执行任务对象,通过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 {
    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0:
                // do something by sharding item 0
                break;
            case 1:
                // do something by sharding item 1
                break;
            case 2:
                // do something by sharding item 2
                break;
            // case n: ...
        }
    }
}

举例:应用A有五个任务需要执行,分别是A,B,C,D,E。任务E需要分成四个子任务,应用部署在两台机器上。如何实现一个任务调度系统(一)-鸿蒙开发者社区应用A在启动后, 5个任务通过 Zookeeper 协调后被分配到两台机器上,通过Quartz Scheduler 分开执行不同的任务。

 

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

 

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

 

ElasticJob 的控制台通过读取注册中心数据展现作业状态,更新注册中心数据修改全局任务配置。从一个任务调度平台的角度来看,控制台功能还是偏孱弱的。

已于2022-5-17 17:46:06修改
收藏
回复
举报
回复
    相关推荐