分布式调度Elastic-job(保姆级)上

1.概述

        1.1什么是任务调度

                我们可以思考⼀下下⾯业务场景的解决⽅案:               

                        1:某电商平台需要每天上午10点,下午3点,晚上8点发放⼀批优惠券

                        2:某银⾏系统需要在信⽤卡到期还款⽇的前三天进⾏短信提醒

                        3:某财务系统需要在每天凌晨0:10分结算前⼀天的财务数据,统计汇总

                以上场景就是任务调度所需要解决的问题

任务调度是为了⾃动完成特定任务,在约定的特定时刻去执⾏任务的过程

        项⽬的学习中,使⽤过Spring中提供的定时任务注解@Scheduled在业务类中⽅法中贴上这个注解

@Scheduled(cron = "0/20 * * * * ? ")
public void doWork(){
    //doSomething
}

   然后在启动类上贴上 @EnableScheduling 注解

1.2 为什么需要分布式调度

感觉Spring给我们提供的这个注解可以完成任务调度的功能,好像已经完美解决问题了,为什么还需要分布式呢?

主要有如下这⼏点原因:

        1.单机处理极限:原本1分钟内需要处理1万个订单,但是现在需要1分钟内处理10万个订单;原来⼀个统计需要1⼩时,现在业务⽅需要10分钟就统计出来。你也许会说,你也可以多线程、单机多进程处理。的确,多线程并⾏处理可以提⾼单位时间的处理效率,但是单机能⼒毕竟有限(主要是CPU、内存和磁盘),始终会有单机处理不过来的情况。

        2.⾼可⽤:单机版的定式任务调度只能在⼀台机器上运⾏,如果程序或者系统出现异常就会导致功能不可⽤。虽然可以在单机程序实现的⾜够稳定,但始终有机会遇到⾮程序引起的故障,⽽这个对于⼀个系统的核⼼功能来说是不可接受的。

        3.防⽌重复执⾏: 在单机模式下,定时任务是没什么问题的。但当我们部署了多台服务,同时⼜每台服务⼜有定时任务时,若不进⾏合理的控制在同⼀时间,只有⼀个定时任务启动执⾏,这时,定时执⾏的结果就可能存在混乱和错误了

这个时候就需要分布式的任务调度来实现了。

1.3 Elastic-Job介绍

        Elastic-Job是⼀个分布式调度的解决⽅案,由当当⽹开源,它由两个相互独⽴的⼦项⽬Elastic-job-Lite和Elastic-Job-Cloud组成,使⽤Elastic-Job可以快速实现分布式任务调度。

Elastic-Job的地址: ElasticJob - Distributed scheduled job solution

功能列表:

        1:分布式调度协调

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

        2:丰富的调度策略:

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

        3:弹性拓容缩容

                当集群中增加⼀个实例,它应当能够被选举被执⾏任务;当集群减少⼀个实例时,他所执⾏的任务能被转移到别的示例中执⾏。

        4:失效转移

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

        5:错过执⾏任务重触发

                若因某种原因导致作业错过执⾏,⾃动记录错误执⾏的作业,并在下次次作业完成后⾃动触发。

        6:⽀持并⾏调度

                ⽀持任务分⽚,任务分⽚是指将⼀个任务分成多个⼩任务在多个实例同时执⾏。

        7:作业分⽚⼀致性

                当任务被分⽚后,保证同⼀分⽚在分布式环境中仅⼀个执⾏实例。

        8:⽀持作业⽣命周期操作

                可以动态对任务进⾏开启及停⽌操作。

        9:丰富的作业类型

                ⽀持Simple、DataFlow、Script三种作业类型

系统架构图

2.Elastic-Job快速⼊⻔

2.1 环境搭建

2.1.1 版本要02.求

        1:JDK 要求1.7以上保本

        2:Maven 要求3.0.4及以上版本

        3:Zookeeper 要求采取3.4.6以上版本

2.1.2 Zookeeper安装&运⾏

        把Zookeeper解压,并进到conf文件夹里面,把zoo_sample.cfg在本文件夹里复制一份,把名称改为zoo.cfg,然后退到上一个文件目录,找到bin文件夹,在bin文件夹里双击 zkServer.cmd  然后解压ZooInspector.zip,他是可视化Zookeeper的内容,解压完成以后。进入到build文件夹里,在该文件夹里输入cmd,然后在黑窗口输入   java -jar zookeeper-dev-ZooInspector.jar,然后就会出现一个可视化界面

2.1.3 创建Maven项⽬

<dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>elastic-job-lite-core</artifactId>
    <version>2.1.5</version>
</dependency>

2.2 代码实现

2.2.1 任务类

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;


import java.util.Date;


/**
* <h1></h1>
*
* @version 1.0
* @date 2022-12-8 15:04
*/
public class MyElasticJob implements SimpleJob {


    @Override
    public void execute(ShardingContext shardingContext) {
        System.out.println("定时任务开始====>"+new Date());
    }
}

2.2.2 配置类

import cn.wolfcode.MyElasticJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.api.JobScheduler;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;


/**
* <h1></h1>
*
* @version 1.0
* @date 2022-12-8 15:06
*/
public class JobDemo {


    public static void main(String[] args) {
        //注册中心对象,任务配置对象
        new JobScheduler(createRegistryCenter(), createJobConfiguration()).init();
    }


    //定时任务类配置
    private static LiteJobConfiguration createJobConfiguration() {
        // 定义作业核⼼配置  JobCoreConfiguration.newBuilder("任务名称","多长时间执行一次","分片数量")
        JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder("myElasticJob","0/5 * * * * ?",1).build();
        // 定义SIMPLE类型配置  SimpleJobConfiguration("配置名称","执行哪一个处理类")  MyElasticJob.class.getCanonicalName() 这个方法是获取全限类名
        SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, MyElasticJob.class.getCanonicalName());
        // 定义Lite作业根配置
        // LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();  加.overwrite(true)是表示可以覆盖之前写的配置,不加这个的话,那你改的定时器执行时间是不会有变化的
        LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
        return simpleJobRootConfig;
    }


    //注册中心配置
    private static CoordinatorRegistryCenter createRegistryCenter() {
        //ZookeeperConfiguration("配置zk地址","调度任务的组名")
        ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181", "elastic-job-demo");
        //设置节点超时时间
        zookeeperConfiguration.setSessionTimeoutMilliseconds(100);
        CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
        regCenter.init();
        return regCenter;
    }
}

2.2.3 测试

运行成功以后 可视化页面会多一个文件

3.SpringBoot集成Elastic-Job

3.1 添加Maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>
<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>
    </dependency>
</dependencies>

代码实现

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.stereotype.Component;


import java.util.Date;


/**
* <h1></h1>
*
* @author < a href=" ">刘军威</ a>
* @version 1.0
* @date 2022-12-8 16:41
*/
@Component
public class MyElasticJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
        System.out.println("定时调度:"+new Date());
    }
}
import cn.wolfcode.job.MyElasticJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
* <h1></h1>
*
* @author < a href=" ">刘军威</ a>
* @version 1.0
* @date 2022-12-8 16:43
*/
@Configuration
public class JobConfig {


    //注册中心配置
    //@Value("${zookeeper.url}") 从配置文件中拿具体的值
    @Bean
    public CoordinatorRegistryCenter registryCenter(@Value("${zookeeper.url}") String url, @Value("${zookeeper.groupName}")String groupName) {
        //ZookeeperConfiguration("配置zk地址","调度任务的组名")
        ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(url, groupName);
        //设置节点超时时间
        zookeeperConfiguration.setSessionTimeoutMilliseconds(100);
        CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
        regCenter.init();
        return regCenter;
    }




    //定时任务类配置  因为项目中有可能需要写很多个定时器,所以这个不能写死,要变成一个公共的方法
    private  LiteJobConfiguration createJobConfiguration(Class clazz,String cron,int shardingCount) {
        // 定义作业核⼼配置  JobCoreConfiguration.newBuilder("任务名称","多长时间执行一次","分片数量")
        JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(clazz.getSimpleName(),cron,shardingCount).build();
        // 定义SIMPLE类型配置  SimpleJobConfiguration("配置名称","执行哪一个处理类")  MyElasticJob.class.getCanonicalName() 这个方法是获取全限类名
        SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, clazz.getCanonicalName());
        // 定义Lite作业根配置
        // LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();  加.overwrite(true)是表示可以覆盖之前写的配置,不加这个的话,那你改的定时器执行时间是不会有变化的
        LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
        return simpleJobRootConfig;
    }




    //测试调度
    @Bean(initMethod = "init")
    public SpringJobScheduler testScheduler(MyElasticJob job,CoordinatorRegistryCenter registryCenter){
        LiteJobConfiguration jobConfiguration = createJobConfiguration(job.getClass(),"0/5 * * * * ?",1);
        return new SpringJobScheduler(job,registryCenter,jobConfiguration);
    }
}

application.yml

zookeeper:
  url: localhost:2181
  groupName: elastic-job-springboot

4.案例需求

        需求:数据库中有⼀些列的数据,需要对这些数据进⾏备份操作,备份完之后,修改数据的状态,标记已经备份了.

4.1 初始化数据

在数据库中导⼊ elastic-job-demo.sql 数据

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50636
Source Host           : localhost:3306
Source Database       : elastic-job-demo

Target Server Type    : MYSQL
Target Server Version : 50636
File Encoding         : 65001

Date: 2020-05-08 11:23:42
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for t_file_custom
-- ----------------------------
DROP TABLE IF EXISTS `t_file_custom`;
CREATE TABLE `t_file_custom` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `content` varchar(255) DEFAULT NULL,
  `type` varchar(255) DEFAULT NULL,
  `backedUp` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_file_custom
-- ----------------------------
INSERT INTO `t_file_custom` VALUES ('1', '文件1', '内容1', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('2', '文件2', '内容2', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('3', '文件3', '内容3', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('4', '文件4', '内容4', 'image', '1');
INSERT INTO `t_file_custom` VALUES ('5', '文件5', '内容5', 'image', '1');
INSERT INTO `t_file_custom` VALUES ('6', '文件6', '内容6', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('7', '文件6', '内容7', 'radio', '1');
INSERT INTO `t_file_custom` VALUES ('8', '文件8', '内容8', 'radio', '1');
INSERT INTO `t_file_custom` VALUES ('9', '文件9', '内容9', 'vedio', '1');
INSERT INTO `t_file_custom` VALUES ('10', '文件10', '内容10', 'vedio', '1');
INSERT INTO `t_file_custom` VALUES ('11', '文件11', '内容11', 'vedio', '1');
INSERT INTO `t_file_custom` VALUES ('12', '文件12', '内容12', 'vedio', '1');
INSERT INTO `t_file_custom` VALUES ('13', '文件13', '内容13', 'image', '1');
INSERT INTO `t_file_custom` VALUES ('14', '文件14', '内容14', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('15', '文件15', '内容15', 'image', '1');
INSERT INTO `t_file_custom` VALUES ('16', '文件16', '内容16', 'text', '1');
INSERT INTO `t_file_custom` VALUES ('17', '文件17', '内容17', 'radio', '1');
INSERT INTO `t_file_custom` VALUES ('18', '文件18', '内容18', 'image', '1');
INSERT INTO `t_file_custom` VALUES ('19', '文件19', '内容19', 'radio', '1');
INSERT INTO `t_file_custom` VALUES ('20', '文件20', '内容20', 'vedio', '1');

4.2 集成Druid&MyBatis

4.2.1 添加依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>
<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>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

4.2.2 添加配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/elastic-job-demo?serverTimezone=GMT%2B8
    driverClassName: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root

4.2.3 添加实体类

import lombok.Data;


/**
* <h1></h1>
*
* @version 1.0
* @date 2022-12-8 17:22
*/
@Data
public class FileCustom {
    //唯⼀标识
    private Long id;
    //⽂件名
    private String name;
    //⽂件类型
    private String type;
    //⽂件内容
    private String content;
    //是否已备份
    private Boolean backedUp = false;
    public FileCustom(){}
    public FileCustom(Long id, String name, String type, String content){
        this.id = id;
        this.name = name;
        this.type = type;
        this.content = content;
    }
}

4.2.4 添加Mapper处理类

import cn.wolfcode.domain.FileCustom;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;


import java.util.List;


/**
* <h1></h1>
*
* @version 1.0
* @date 2022-12-8 17:24
*/
@Mapper
public interface FileCustomMapper {
    @Select("select * from t_file_custom where backedUp = 0")
    List<FileCustom> selectAll();
    @Update("update t_file_custom set backedUp = #{state} where id = #{id}")
    int changeState(@Param("id") Long id, @Param("state")int state);
}

4.3 业务功能实现

4.3.1 添加任务类

import cn.wolfcode.domain.FileCustom;
import cn.wolfcode.mapper.FileCustomMapper;
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


import java.util.List;
import java.util.concurrent.TimeUnit;


/**
* <h1></h1>
*
* @version 1.0
* @date 2022-12-8 17:27
*/
@Component
public class FileCustomElasticJob implements SimpleJob {
    @Autowired
    private FileCustomMapper fileCustomMapper;
    @Override
    public void execute(ShardingContext shardingContext) {
        doWork();
    }
    private void doWork(){
        List<FileCustom> fileList = fileCustomMapper.selectAll();
        System.out.println("需要备份⽂件个数:"+fileList.size());
        for(FileCustom fileCustom:fileList){
            backUpFile(fileCustom);
        }
    }
    private void backUpFile(FileCustom fileCustom){
        try {
            //模拟备份动作
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执⾏⽂件备份====>"+fileCustom);
        fileCustomMapper.changeState(fileCustom.getId(),1);
    }
}

4.3.2 添加任务调度配置

在配置类中新增这个Bean

@Bean(initMethod = "init")
public SpringJobScheduler fileScheduler(FileCustomElasticJob job, CoordinatorRegistryCenter registryCenter){
    LiteJobConfiguration jobConfiguration = createJobConfiguration(job.getClass(),"0 0/1  * * * ?",1);
    return new SpringJobScheduler(job,registryCenter,jobConfiguration);
}

4.4 测试&问题

        为了⾼可⽤,我们会对这个项⽬做集群的操作,可以保证其中⼀台挂了,另外⼀台可以继续⼯作.但是在集群的情况下,调度任务只在⼀台机器上运⾏,如果单个任务调度⽐较耗时,耗资源的情况下,对这台机器的消耗还是⽐较⼤的,但是这个时候,其他机器却是空闲着的.如何合理的利⽤集群的其他机器且如何让任务执⾏得更快些呢?这时候Elastic-Job提供了任务调度分⽚的功能.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咸鱼的动力

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

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

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

打赏作者

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

抵扣说明:

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

余额充值