Elastic-Job分布式任务调度

Elastic-Job分布式任务调度

1.概述

1.1 什么是任务调度

我们可以先思考一下业务场景的解决方案:

  • 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
  • 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
  • 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
  • 12306会根据车次的不同,设置某几个时间点进行分批放票。
  • 某网站为了实现天气实时展示,每隔10分钟就去天气服务器获取最新的实时天气信息。

以上业务场景的解决方案就是任务调度。

任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。

如何实现任务调度?

  • 多线程方式,结合sleep
  • JDK提供的API,例如:Timer、ScheduledExecutor
  • 框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求

但是上述这些解决方案要么实现起来比较繁琐,要么不能满足分布式架构需求,我们需要更好的解决方案。

1.2 什么是分布式任务调度

当前软件的架构已经开始向分布式架构转变,将单体结构拆分为若干服务,服务之间通过网络交互来完成业务处理。在分布式架构下,一个服务往往会部署多个实例来运行我们的业务,如果在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度

将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力:

1、并行任务调度

并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。

如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

2、高可用

若某一个实例宕机,不影响其他实例来执行任务。

3、弹性扩容

当集群中增加实例就可以提高并执行任务的处理效率。

4、任务管理与监测

对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

分布式任务调度面临的问题:

当任务调度以集群方式部署,同一个任务调度可能会执行多次,例如:电商系统定期发放优惠券,就可能重复发放优惠券,对公司造成损失,信用卡还款提醒就会重复执行多次,给用户造成烦恼,所以我们需要控制相同的任务在多个运行实例上只执行一次。常见解决方案:

  • 分布式锁,多个实例在任务执行前首先需要获取锁,如果获取失败那么就证明有其他服务已经在运行,如果获取成功那么证明没有服务在运行定时任务,那么就可以执行。
  • ZooKeeper选举,利用ZooKeeper对Leader实例执行定时任务,执行定时任务的时候判断自己是否是Leader,如果不是则不执行,如果是则执行业务逻辑,这样也能达到目的。

1.3 Elastic-Job简介

针对分布式任务调度的需求,市场上出现了很多的产品:

1) TBSchedule:淘宝推出的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。但是已经多年未更新,文档缺失严重,缺少维护。

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

3)Elastic-job:当当网借鉴TBSchedule并基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,具有任务高可用以及分片功能。

4)Saturn: 唯品会开源的一个分布式任务调度平台,基于Elastic-job,可以全域统一配置,统一监
控,具有任务高可用以及分片功能。

Elastic-Job是一个分布式调度的解决方案,由当当网开源,它由两个相互独立的子项目Elastic-Job-Lite和Elastic- Job-Cloud组成,使用Elastic-Job可以快速实现分布式任务调度。Elastic-Job的github地址:https://github.com/elasticjob。

功能列表:

分布式调度协调

在分布式环境中,任务能够按指定的调度策略执行,并且能够避免同一任务多实例重复执行。

丰富的调度策略:

基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。

弹性扩容缩容

当集群中增加某一个实例,它应当也能够被选举并执行任务;当集群减少一个实例时,它所执行的任务能被转移到别的实例来执行。

失效转移

某实例在任务执行失败后,会被转移到其他实例执行。

错过执行作业重触发

若因某种原因导致作业错过执行,自动记录错过执行的作业,并在上次作业完成后自动触发。

支持并行调度

支持任务分片,任务分片是指将一个任务分为多个小任务项在多个实例同时执行。

支持作业生命周期操作

可以动态对任务进行开启及停止操作。

丰富的作业类型

支持Simple、DataFlow、Script三种作业类型。

Spring整合以及命名空间支持

对Spring支持良好的整合方式,支持spring自定义命名空间,支持占位符。

运维平台

提供运维界面,可以管理作业和注册中心。

1.4 重要概念

  1. 分片

任务的分布式执行,需要将一个任务拆分为多个独立的任务项,然后由分布式的服务器分别执行某一个或几个分片项。例如:有一个遍历数据库某张表的作业,现有2台服务器。为了快速的执行作业,那么每台服务器应执行作业的50%。 为满足此需求,可将作业分成2片,每台服务器执行1片。作业遍历数据的逻辑应为:服务器A遍历ID以奇数结尾的数据;服务器B遍历ID以偶数结尾的数据。 如果分成10片,则作业遍历数据的逻辑应为:每片分到的分片项应为ID%10,而服务器A被分配到分片项0,1,2,3,4;服务器B被分配到分片项5,6,7,8,9,直接的结果就是服务器A遍历ID以0-4结尾的数据;服务器B遍历ID以5-9结尾的数据。

  1. leader选举

    zookeeper会保证在多台服务器中选举出一个leader,leader如果下线会触发重新选举,在选出下个leader前所有任务会被阻塞,
    leader会以“协调者”角色负责分片。

  2. 分片策略

    AverageAllocationJobShardingStrategy

    全路径:

    ​ com.dangdang.ddframe.job.lite.api.strategy.impl.AverageAllocationJobShardingStrategy

    策略说明:

    ​ 基于平均分配算法的分片策略,也是默认的分片策略。

    ​ 如果分片不能整除,则不能整除的多余分片将依次追加到序号小的服务器。如:

    ​ 如果有3台服务器,分成9片,则每台服务器分到的分片是:1=[0,1,2], 2=[3,4,5], 3=[6,7,8]

    ​ 如果有3台服务器,分成8片,则每台服务器分到的分片是:1=[0,1,6], 2=[2,3,7], 3=[4,5]

    ​ 如果有3台服务器,分成10片,则每台服务器分到的分片是:1=[0,1,2,9], 2=[3,4,5], 3=[6,7,8]


    OdevitySortByNameJobShardingStrategy

    全路径:

    ​ com.dangdang.ddframe.job.lite.api.strategy.impl.OdevitySortByNameJobShardingStrategy

    策略说明:

    ​ 根据作业名的哈希值奇偶数决定IP升降序算法的分片策略。

    ​ 作业名的哈希值为奇数则IP升序。

    ​ 作业名的哈希值为偶数则IP降序。

    ​ 用于不同的作业平均分配负载至不同的服务器。

    AverageAllocationJobShardingStrategy的缺点是,一旦分片数小于作业服务器数,作业将永远分配至IP地址靠前
    的服务器,导致IP地址靠后的服务器空闲。而OdevitySortByNameJobShardingStrategy则可以根据作业名称重新
    分配服务器负载。如:

    如果有3台服务器,分成2片,作业名称的哈希值为奇数,则每台服务器分到的分片是:1=[0], 2=[1], 3=[]

    如果有3台服务器,分成2片,作业名称的哈希值为偶数,则每台服务器分到的分片是:3=[0], 2=[1], 1=[]


    RotateServerByNameJobShardingStrategy

    全路径:
    com.dangdang.ddframe.job.lite.api.strategy.impl.RotateServerByNameJobShardingStrategy

    策略说明:

    ​ 根据作业名的哈希值对服务器列表进行轮转的分片策略。

  3. cron表达式

    cron表达式是一个字符串, 用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:

    组成部分含义取值范围
    第一部分Seconds (秒)0-59
    第二部分Minutes(分)0-59
    第三部分Hours(时)0-23
    第四部分Day-of-Month(天)1-31
    第五部分Month(月)0-11或JAN-DEC
    第六部分Day-of-Week(星期)1-7(1表示星期日)或SUN-SAT
    第七部分Year(年) 可选1970-2099

    另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:

    符号含义
    ?表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?”
    *代表所有可能的值
    ,设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务
    -设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务
    /设置频率或间隔,如"1/15"表示从1分开始,每隔15分钟运行一次任务
    L用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如"6L"表示"每月的最后一个星期五"
    W表示离给定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日"
    #表示该月第几个周X。例如”6#3”表示该月第3个周五

    为了让大家更熟悉cron表达式的用法, 接下来我们给大家列举了一些例子, 如下表所示:

    cron表达式含义
    */5 * * * * ?每隔5秒运行一次任务
    0 0 23 * * ?每天23点运行一次任务
    0 0 1 1 * ?每月1号凌晨1点运行一次任务
    0 0 23 L * ?每月最后一天23点运行一次任务
    0 26,29,33 * * * ?在26分、29分、33分运行一次任务
    0 0/30 9-17 * * ?朝九晚五工作时间内每半小时运行一次任务
    0 15 10 ? * 6#3每月的第三个星期五上午10:15运行一次任务

2.Elastic-Job快速入门

在分布式架构下,通过Elastic-Job实现分片查询数据的定时任务

2.1 环境搭建

  1. 版本要求
  • JDK要求1.7及以上版本
  • Maven要求3.0.4及以上版本
  • zookeeper要求采用3.4.6及以上版本
  1. Zookeeper安装&运行

    Zookeeper类似于Eureka,用来进行微服务注册和管理。

  1. 数据库

    使用mybatis-plus入门案例中的数据库mp,数据表为tb_user

    在这里插入图片描述

2.2 入门案例

  1. 新建maven工程

在这里插入图片描述

  1. 引入依赖
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <dependency>
        <groupId>com.dangdang</groupId>
           <artifactId>elastic-job-lite-spring</artifactId>
           <version>2.1.5</version>
       </dependency>
   
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
    </dependency>
   
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.1.1</version>
       </dependency>
   
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>5.1.47</version>
       </dependency>
   
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-log4j12</artifactId>
       </dependency>
   </dependencies>
  1. 编写springBoot配置文件及启动类
    springBoot 配置文件:

    server.port=${PORT:57081}
    spring.application.name = elastic-job-springboot
    logging.level.root = info
    
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/mp?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=123
    
    # 设置Mapper接口所对应的XML文件位置
    mybatis-plus.mapper-locations = classpath*:dao/*.xml
    # 设置别名包扫描路径
    mybatis-plus.type-aliases-package = com.itheima.elasticjob.pojo
    
    # zookeeper服务地址
    zookeeper.connString = localhost:2181
    # 名称空间
    myjob.namespace = elastic-job-example
    # 分片总数
    myjob.count = 3
    # cron表达式(定时策略)
    myjob.cron = 0/5 * * * * ?
    

    springBoot 启动类:

    @MapperScan("com.itheima.elasticjob.dao")
    @SpringBootApplication
    public class ElasticJobApp {
        public static void main(String[] args) {
         SpringApplication.run(ElasticJobApp.class,args);
        }
    }
    
  2. 实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @TableName("tb_user")
    public class User {
        @TableId("ID")
        private Long id;
        @TableField("USER_NAME")
        private String userName;
        @TableField("PASSWORD")
        private String password;
        @TableField("NAME")
        private String name;
        @TableField("AGE")
        private Integer age;
    }
    
  3. 数据访问层

    public interface UserMapper extends BaseMapper<User> {
        //对id通过mod函数取余进行分片
        @Select("SELECT *  FROM `tb_user` WHERE MOD(id,#{shardingTotalCount})=#{shardingItem}")
        List<User> queryUserById(@Param("shardingTotalCount") int count, @Param("shardingItem") int item);
     }
    
  4. Elastic-Job任务类

    /**
     * 数据查询任务
     **/
    @Component
    public class MyJob implements SimpleJob {
    
        @Autowired
        private UserMapper userMapper;
    
        //执行定时任务
        @Override
        public void execute(ShardingContext shardingContext) {
            //得到分片总数
            int count = shardingContext.getShardingTotalCount();
            //得到分片项
            int item = shardingContext.getShardingItem();
    
            //查询数据
            List<User> userList = userMapper.queryUserById(count,item);
    
            //输出结果
            userList.forEach(user -> {
                System.out.println("作业分片:"+item+"--->"+user);
            });
        }
    }
    
  5. zookeeper注册中心

    @Configuration
    public class ZKRegistryCenterConfig {
    
    	//zookeeper服务地址
        @Value("${zookeeper.connString}")
        private  String ZOOKEEPER_CONNECTION_STRING ;
    
        //定时任务命名空间
        @Value("${myjob.namespace}")
        private  String JOB_NAMESPACE;
    
        //zk的配置及创建注册中心
        @Bean(initMethod = "init")
        public  ZookeeperRegistryCenter setUpRegistryCenter(){
            //zk的配置
            ZookeeperConfiguration zookeeperConfiguration = new 
                ZookeeperConfiguration(ZOOKEEPER_CONNECTION_STRING, JOB_NAMESPACE);
            //创建注册中心
            ZookeeperRegistryCenter zookeeperRegistryCenter = new 
                ZookeeperRegistryCenter(zookeeperConfiguration);
            return zookeeperRegistryCenter;
        }
    }
    
  6. elastic-job配置类

    @Configuration
    public class ElasticJobConfig {
    
        @Autowired
        MyJob myJob;
    
        @Autowired
        ZookeeperRegistryCenter registryCenter;
    
        @Value("${myjob.count}")
        private int shardingCount;
    
        @Value("${myjob.cron}")
        private String cron;
    
        /**
         * 配置任务详细信息
         * @param jobClass 任务执行类
         * @param cron  执行策略
         * @param shardingTotalCount 分片数量
         * @return
         */
        private LiteJobConfiguration createJobConfiguration(final Class<? extends SimpleJob> jobClass,
                                                            final String cron,
                                                            final int shardingTotalCount){
            //创建JobCoreConfigurationBuilder
            JobCoreConfiguration.Builder JobCoreConfigurationBuilder = 
                JobCoreConfiguration.newBuilder(jobClass.getName(), cron, shardingTotalCount);
    
            JobCoreConfiguration jobCoreConfiguration = JobCoreConfigurationBuilder.build();
            //创建SimpleJobConfiguration
            SimpleJobConfiguration simpleJobConfiguration = 
                new SimpleJobConfiguration(jobCoreConfiguration, jobClass.getCanonicalName());
            //创建LiteJobConfiguration
            LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(simpleJobConfiguration)                .jobShardingStrategyClass("com.dangdang.ddframe.job.lite.api.strategy.impl.AverageAllocationJobShardingStrategy").overwrite(true)
                    .build();
            return liteJobConfiguration;
        }
    
        @Bean(initMethod = "init")
        public SpringJobScheduler initSimpleElasticJob() {
            //创建SpringJobScheduler
            SpringJobScheduler springJobScheduler = new SpringJobScheduler(myJob, registryCenter,
                    createJobConfiguration(myJob.getClass(), cron, shardingCount));
            return springJobScheduler;
        }
    }
    

2.3 运行效果

由于在配置文件中设置的分片数量为3,所以这里启动了三个微服务,启动过程中会花费一些时间向zookeeper进行注册。运行起来后,我们会发现三个服务微每隔5秒钟就执行一次数据查询,并且进行了分片(平均分配)。

在这里插入图片描述

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是二次元穿越来的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值