分布式任务调度——Elastic-Job

在实际项目开发中,定时任务调度是不可缺少的,在Spring Boot中提供了 @Scheduled 注解实现了定时调度的功能,其用法也是非常的简单如下:

@SpringBootApplication
@EnableScheduling
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(QuartzApplication.class, args);
	}

	/**
	 * initialDelay(和initialDelayString),第一次调度之前需要延迟多久
	 * fixedRate(和fixedRateString),每隔多久时间需要调度一次
	 * fixedDelay(和fixedDelayString),两次调度之间需要加一个固定的延迟时间
	 * zone,解析cron表达式的时候解析时区
	 */

	//第一次执行之前延后10秒钟;后续每隔5秒执行1次
	@Scheduled(fixedRate = 5000, initialDelay = 10000)
	public void task1() {
		SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
		System.out.println("当前时间:" + dateFormat.format(new Date()));
	}

	//使用cron表达式,每5秒执行一次
	@Scheduled(cron = "0/5 * *  * * ?")
	public void task2() {
		SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
		System.out.println("当前时间:" + dateFormat.format(new Date()));
	}

}

SpringBoot中设置调度任务非常的简单,其实其实现并不难,我们也可以自己来实现,如 TimerScheduledExecutorQuartz ,这里我们重点介绍了功能强大的Quartz,其实现简单,并且支持高可用,可多节点部署,通过竞争数据库锁来保证只有一个节点执行任务。
分布式任务调度——Elastic-Job_第1张图片

  • SchedulerFactoryBean: 调度工厂,默认是StdSchedulerFactory,通过getScheduler()得到调度器Scheduler
  • Scheduler: 任务调度,代表一个任务调度器,用于组合Trigger
  • Trigger: 触发器,执行任务的规则,代表一个调度参数的配置,用于组合JobDetail(什么时间与间隔调用)
  • JobDetail: 任务的细节,表示一个具体的可执行的调度程序,用于组合Job(任务方案,名称,策略)
  • Job: 任务,表示一个要执行具体内容的工作(任务内容)

有关具体的介绍,可查看 定时任务框架Quartz


这里我们就来简单的看一看在SpringBoot中如何使用Quartz,首先需要引入相关依赖,如下:

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-quartzartifactId>
dependency>

然后我们先来定义一个处理业务逻辑的Job,如下:

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String value = dataMap.get("name").toString();
        System.out.println(value);
    }
}

再在项目中使用 @Bean 的方式将其注入即可,还是定时任务框架Quartz中介绍的相关内容,如下:

@SpringBootApplication
@EnableScheduling
public class QuartzApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuartzApplication.class, args);
    }

    @Bean("jobDetail")
    public JobDetailFactoryBean jobDetailFactoryBean() {
        JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
        jobDetailFactoryBean.setJobClass(MyJob.class);
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("name", "MyQuartz");
        jobDetailFactoryBean.setJobDataAsMap(jobDataMap);
        return jobDetailFactoryBean;
    }

    @Bean("cronJobTrigger")
    public CronTriggerFactoryBean cronTriggerFactoryBean(JobDetailFactoryBean jobDetail) {
        CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
        triggerFactoryBean.setJobDetail(jobDetail.getObject());
        triggerFactoryBean.setCronExpression("0/2 * * * * ?");
        return triggerFactoryBean;
    }

    /**
     * 调度工厂,默认是StdSchedulerFactory
     * 通过getScheduler()得到调度器----StdScheduler
     * Scheduler中有一个QuartzSchedulerThread线程实例---即调度线程
     * 调度线程---不停地获取触发器---当触发器触发时,从线程池中取线程执行对应的job
     */
    @Bean("scheduler")
    public SchedulerFactoryBean schedulerFactoryBean(Trigger cronJobTrigger) {
        SchedulerFactoryBean bean = new SchedulerFactoryBean();
        bean.setOverwriteExistingJobs(true);
        bean.setStartupDelay(1);
        bean.setTriggers(cronJobTrigger);
        return bean;
    }
}



随着系统规模的发展,系统可能从单机部署到集群部署,在集群中调度任务对集群机器中配置的任务,只能运行一个(排它处理),另外随着业务越来越多,相应的定时任务也会增多,单台服务器执行任务的压力会越来越大

  • 不敢轻易跟着应用服务多节点部署,可能会重复多次执行而引发系统逻辑的错误。
  • Quartz的集群仅仅只是用来HA,节点数量的增加并不能给我们的每次执行效率带来提升,即不能实现水平扩展。

这里我们想到的最简单的方式就是加锁或者进行选举。 如上述介绍的Quartz在分布式环境下,就可以通过竞争数据库锁来保证只有一个节点执行任务。但是也存在缺点,比如性能差,无法适应高并发场景;容易出现死锁的情况;无法优雅的实现阻塞式锁。


不过这里我们就无需自己来处理,分布式下任务调度有很多开源的解决方案,如Elastic-Job 、xxl-job 等等,这里我们就先来了解下Elastic-Job框架。


Elastic job是当当网开源提供的开源分布式调度工具,其基于Zookeeper、Quartz开发并开源的一个Java分布式定时任务,解决了Quartz不支持分布式的弊端。Elastic job主要的功能有支持弹性扩容,通过Zookepper集中管理和监控job,支持失效转移等。项目由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成,其最大的区别是有无调度中心。


Elastic job会将直接把任务分摊出去,每台机器负责哪些任务都会分配好,使用Elastic job首先我们需要引入相关依赖,如下:


<dependency>
    <groupId>com.dangdanggroupId>
    <artifactId>elastic-job-lite-coreartifactId>
    <version>2.1.5version>
dependency>


<dependency>
    <groupId>com.dangdanggroupId>
    <artifactId>elastic-job-lite-springartifactId>
    <version>2.1.5version>
dependency>

然后就可以来实现业务处理的逻辑,Job可以有好几种作业类型,如果只是简单的任务,实现S impleJob 即可,并且这里我们会做任务分片处理,如下:

public class MyJob implements SimpleJob {

    @Override
    public void execute(ShardingContext shardingContext) {

        System.out.println("当前分片:" + shardingContext.getShardingParameter());

        int shardingItem = shardingContext.getShardingItem();
        int total = shardingContext.getShardingTotalCount();

        if (shardingItem == 1) {
            System.out.println(String.format("ThreadId:%s, 当前分片项:%s", Thread.currentThread().getId(), shardingItem));
        } else {
            System.out.println(String.format("ThreadId:%s, 当前分片项:%s", Thread.currentThread().getId(), shardingItem));
        }
    }
}

任务分片解释:某些量大的任务,能够通过分拆分摊,提高执行时效,如:给1万用户发短信或邮件等,此时,可做为分片任务来处理,即任务分拆成多片小任务。任务触发时,每片任务发送到一台机器去处理,如1万用户发短信,拆成两个任务(每个发5000条短信),分别到达两台机器去执行。


然后我们还需要对Elastic-Job进行相关的配置,如下:


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:reg="http://www.dangdang.com/schema/ddframe/reg"
       xmlns:job="http://www.dangdang.com/schema/ddframe/job"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.dangdang.com/schema/ddframe/reg
                        http://www.dangdang.com/schema/ddframe/reg/reg.xsd
                        http://www.dangdang.com/schema/ddframe/job
                        http://www.dangdang.com/schema/ddframe/job/job.xsd">

    
    <reg:zookeeper id="jobRegisterCenter"
                   server-lists="127.0.0.1:2181"
                   namespace="elastic-job"
                   base-sleep-time-milliseconds="1000"
                   max-sleep-time-milliseconds="3000"
                   max-retries="3"/>

    
    <job:simple id="testJob" class="com.kami.MyJob"
                registry-center-ref="jobRegisterCenter"
                cron="0/5 * *  * * ?"
                disabled="false"
                overwrite="true"
                sharding-total-count="2"
                sharding-item-parameters="0=A,1=B"
                description="测试Elastic Job"/>
beans>

这里我们如果启动一个项目,那么分片就暂不会发挥作用,这里我们启动两个任务,启动类如下:

@SpringBootApplication
@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplicationA {
    public static void main(String[] args) {
        SpringApplication.run(ElasticJobApplicationA.class, args);
    }
}
@SpringBootApplication
@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplicationB {
    public static void main(String[] args) {
        SpringApplication.run(ElasticJobApplicationB.class, args);
    }
}

这里我们启动两个项目时,需要更改 application.yml 中的端口配置server.port,并且在启动一个启动类时,需要将另一个启动类上注解暂时注释掉,否则会报异常,测试结果如下:
分布式任务调度——Elastic-Job_第2张图片
分布式任务调度——Elastic-Job_第3张图片


这里如果我们将其中一个项目停止,那么所有的任务就会有同一个项目进行处理(需等待一段时间),如下:
分布式任务调度——Elastic-Job_第4张图片




上述是使我们使用的xml方式来配置的,如果想要更加简洁的配置,我们可以通过一个GitHub上的项目来实现,GitHub地址:https://github.com/yinjihuan/elastic-job-spring-boot-starter,这个需要我们添加仓库地址之后,才可以拉取到相关的依赖。


所以首先我们需要在maven的安装路径下的 conf 文件下的 settings.xml 文件中,添加如下仓库地址

<repositories>
	<repository>
        <id>jitpack.ioid>
        <url>https://jitpack.iourl>
	repository>
repositories>

然后就可以来取到相关的依赖了,如下:

<dependency>
    <groupId>com.cxytiandigroupId>
    <artifactId>elastic-job-spring-boot-starterartifactId>
    <version>1.0.4version>
dependency>

然后我们就无需配置xml文件了,直接在 yml 中配置即可,如下:

server:
  port: 8080

elastic:
  job:
    zk:
      serverLists: 127.0.0.1:2181
      namespace: elastic-job

    myJob:
      cron: 0/5 * * * * ?
      shardingTotalCount: 2
      shardingItemParameters: 0=A,1=B

这里我们在启动类中就无需引入xml文件了,不过这里我们需要加上 @EnableElasticJob 注解,如下:

@SpringBootApplication
@EnableElasticJob
//@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplication {
    public static void main(String[] args) {
        SpringApplication.run(ElasticJobApplication.class, args);
    }
}

你可能感兴趣的:(分布式架构)