1 概述

  首先了解下分布式调度:

  1)运⾏在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执 ⾏)

   2)分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个⼩的作 业任务,同时执⾏)

  单体应用中所有的定时任务都在一个服务器的一套程序中运行,拆分成分布式之后,不同的定时任务会拆分到不同的子系统中去,而不同的子系统又部署了多份,那么问题来了,比如统计报表定时任务,如果两个定时任务同时跑,会不会重复处理?

定时任务与消息队列区别?

共同点:

  1 异步处理,比如注册事件

  2 应用解耦,不管定时任务作业还是MQ都可以作为两个应⽤之间的⻮轮实现应⽤解耦,这个⻮轮可以中转 数据,当然单体服务不需要考虑这些,服务拆分的时候往往都会考虑

  3 流量削峰, 双⼗⼀的时候,任务作业和MQ都可以⽤来扛流量,后端系统根据服务能⼒定时处理订单或者 从MQ抓取订单抓取到⼀个订单到来事件的话触发处理,对于前端⽤户来说看到的结果是已经 下单成功了,下单是不受任何影响的。

不同点:

  本质不同:定时任务作业是时间驱动,⽽MQ是事件驱动; 时间驱动是不可代替的,⽐如⾦融系统每⽇的利息结算,不是说利息来⼀条(利息到来事件)就算 ⼀下,⽽往往是通过定时任务批量计算;

  定时任务倾向于批处理,MQ倾向于逐条处理。

2 实现方式

  定时任务的实现⽅式有多种。早期没有定时任务框架的时候,我们会使⽤JDK中的Timer机制和多线程机 制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执⾏某⼀段程序;后来有了定时任务框架,⽐ 如⼤名鼎鼎的Quartz任务调度框架,使⽤时间表达式(包括:秒、分、时、⽇、周、年)配置某⼀个任 务什么时间去执⾏。

2.1 Quartz 

可以在单机上运行。

添加依赖:

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>1.7.3</version>
        </dependency>

 

配置文件:

<?xml version='1.0' encoding='utf-8'?>
<quartz xmlns="http://www.opensymphony.com/quartz/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.opensymphony.com/quartz/JobSchedulingData
  http://www.opensymphony.com/quartz/xml/job_scheduling_data_1_5.xsd"
    version="1.5">
    <calendar class-name="org.quartz.impl.calendar.HolidayCalendar"
        replace="true">
        <name>holidayCalendar</name>
        <description>HolidayCalendar</description>
        <base-calendar class-name="org.quartz.impl.calendar.WeeklyCalendar">
            <name>weeklyCalendar</name>
            <description>WeeklyCalendar</description>
            <base-calendar class-name="org.quartz.impl.calendar.AnnualCalendar">
                <name>annualCalendar</name>
                <description>AnnualCalendar</description>
            </base-calendar>
        </base-calendar>
    </calendar>
    <job>
        <job-detail>
            <name>NewDayJob</name>
            <group>group1</group>
            <description>跨天重置</description>
            <job-class>cn.game.jobs.NewDayJob
            </job-class>
            <volatility>false</volatility>
            <durability>false</durability>
            <recover>false</recover>
        </job-detail>
        <trigger>
            <cron>
                <name>NewDayJob</name>
                <group>group1</group>
                <description>每日</description>
                <job-name>Job-NewDayJob</job-name>
                <job-group>job-group1</job-group>
                <cron-expression>0 0 0 * * ?</cron-expression>
            </cron>
        </trigger>
    </job>
  
</quartz>
 

calendar中使用了HolidayCalendar用于从 Trigger 中排除节假日。

AnnualCalendar(天):指定每年的哪一天。使用方式如上例。

CronCalendar(秒):指定Cron表达式。精度取决于Cron表达式。

DailyCalendar(毫秒):指定每天的时间段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]]

HolidayCalendar(天):指定特定的日期,比如20140613。

MonthlyCalendar(天):指定每月的几号。可选值为1-31。

WeeklyCalendar(天):指定每星期的星期几,可选值比如为java.util.Calendar.SUNDAY。

通过job可以添加多个定时任务,如上添加了跨天重置的定时任务,每日5点重置,在cron-expression中配置表达式。

{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}

范例:

       每隔5秒执行一次:*/5 * * * * ?

                 每隔1分钟执行一次:0 */1 * * * ?

                 每天23点执行一次:0 0 23 * * ?

                 每天凌晨1点执行一次:0 0 1 * * ?

                 每月1号凌晨1点执行一次:0 0 1 1 * ?

                 每月最后一天23点执行一次:0 0 23 L * ?

                 每周星期天凌晨1点实行一次:0 0 1 ? * L

                 在26分、29分、33分执行一次:0 26,29,33 * * * ?

                 每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

cn.game.jobs.NewDayJob类实现了Job类并重写了execute方法,在方法中可以写任务逻辑。

2 分布式调度框架elastic-job

2.1 介绍

  Elastic-Job是当当⽹开源的⼀个分布式调度解决⽅案,基于Quartz⼆次开发的,由两个相互独⽴的⼦项 ⽬Elastic-Job-Lite和Elastic-Job-Cloud组成。我们要学习的是 Elastic-Job-Lite,它定位为轻量级⽆中⼼ 化解决⽅案,使⽤Jar包的形式提供分布式任务的协调服务,⽽Elastic-Job-Cloud⼦项⽬需要结合Mesos 以及Docker在云环境下使⽤。

  主要功能介绍:

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

  ·丰富的调度策略 基于成熟的定时任务作业框架Quartz cron表达式执⾏定时任务

  ·弹性扩容缩容 当集群中增加某⼀个实例,它应当也能够被选举并执⾏任务;当集群减少⼀个实例 时,它所执⾏的任务能被转移到别的实例来执⾏。

  ·失效转移 某实例在任务执⾏失败后,会被转移到其他实例执⾏ 错过执⾏作业重触发 若因某种原因导致作业错过执⾏,⾃动记录错过执⾏的作业,并在上次作业 完成后⾃动触发。

  ·⽀持并⾏调度 ⽀持任务分⽚,任务分⽚是指将⼀个任务分为多个⼩任务项在多个实例同时执⾏。

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

2.2 Elastic-Job-Lite应用

  jar包(API) + 安装zk软件

  Elastic-Job依赖于Zookeeper进⾏分布式协调,所以需要安装Zookeeper软件(3.4.6版本以上)。

  1 安装zookeeper

  1)我们使⽤3.4.10版本,在linux平台解压下载的zookeeper-3.4.10.tar.gz

  2)进⼊conf⽬录,cp zoo_sample.cfg zoo.cfg

  3) 进⼊bin⽬录,启动zk服务 启动 ./zkServer.sh start 停⽌ ./zkServer.sh stop 查看状态 ./zkServer.sh status
  2 引入jar包

 

 

   3 定时任务实例

  需求:每隔两秒钟执⾏⼀次定时任务(resume表中未归档的数据归档到resume_bak表中, 每次归档1条记录) 1)resume_bak和resume表结构完全⼀样 2)resume表中数据归档之后不删除,只将state置为"已归档"。

数据库表结构:

CREATE TABLE `resume` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`name` varchar(255) DEFAULT NULL,

`sex` varchar(255) DEFAULT NULL,

`phone` varchar(255) DEFAULT NULL,

`address` varchar(255) DEFAULT NULL,

`education` varchar(255) DEFAULT NULL,

`state` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) )  ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;
定时任务类:

public class BackupJob implements SimpleJob {
 // 定时任务每执⾏⼀次都会执⾏如下的逻辑
 @Override
 public void execute(ShardingContext shardingContext) {
 /*
 从resume数据表查找1条未归档的数据,将其归档到resume_bak
表,并更新状态为已归档(不删除原数据)
 */
 // 查询出⼀条数据
 String selectSql = "select * from resume where
state='未归档' limit 1";
 List<Map<String, Object>> list =
JdbcUtil.executeQuery(selectSql);
 if(list == null || list.size() == 0) {
     return;
 }
Map<String, Object> stringObjectMap = list.get(0);
 long id = (long) stringObjectMap.get("id");
 String name = (String) stringObjectMap.get("name");
 String education = (String) stringObjectMap.get("education");// 更改状态
 String updateSql = "update resume set state='已归档'
where id=?";
 JdbcUtil.executeUpdate(updateSql,id);
 // 归档这条记录
 String insertSql = "insert into resume_bak select * from resume where id=?";
 JdbcUtil.executeUpdate(insertSql,id);
 }
}

主类:

public class ElasticJobMain {
 public static void main(String[] args) {
 // 配置注册中⼼zookeeper,zookeeper协调调度,不能让任务重复执⾏,
  //通过命名空间分类管理任务,对应到zookeeper的⽬录
 ZookeeperConfiguration zookeeperConfiguration = new
ZookeeperConfiguration("localhost:2181","data-archive-job");
 CoordinatorRegistryCenter coordinatorRegistryCenter = new
ZookeeperRegistryCenter(zookeeperConfiguration);
 coordinatorRegistryCenter.init();
// 配置任务
 JobCoreConfiguration jobCoreConfiguration =
JobCoreConfiguration.newBuilder("archive-job","*/2 * * * * ?",1).build();
 SimpleJobConfiguration simpleJobConfiguration = new
SimpleJobConfiguration(jobCoreConfiguration,BackupJob.class.getName());
 // 启动任务
 new JobScheduler(coordinatorRegistryCenter,
LiteJobConfiguration.newBuilder(simpleJobConfiguration).build()).init();
 }
}
public class JdbcUtil {
 //url
 private static String url = "jdbc:mysql://localhost:3306/job?
characterEncoding=utf8&useSSL=false";
 //user
 private static String user = "root";
 //password
 private static String password = "root";
 //驱动程序类
 private static String driver = "com.mysql.jdbc.Driver";
 static {
 try {
 Class.forName(driver);
 } catch (ClassNotFoundException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 }
 public static Connection getConnection() {
 try {
 return DriverManager.getConnection(url, user,
password);
} catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return null;
 }
 public static void close(ResultSet rs, PreparedStatement ps,
Connection con) {
 if (rs != null) {
 try {
 rs.close();
 } catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 } finally {
 if (ps != null) {
 try {
 ps.close();
 } catch (SQLException e) {
 // TODO Auto-generated catch block
e.printStackTrace();
 } finally {
 if (con != null) {
 try {
 con.close();
 } catch (SQLException e) {
 // TODO Auto-generated catch block
e.printStackTrace();
 }
 }
 }
 }
 }
 }
 }
 public static void executeUpdate(String sql,Object...obj) {
 Connection con = getConnection();
 PreparedStatement ps = null;
 try {
 ps = con.prepareStatement(sql);
 for (int i = 0; i < obj.length; i++) {
 ps.setObject(i + 1, obj[i]);
 }
 ps.executeUpdate();
} catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 } finally {
 close(null, ps, con);
 }
 }
 
 public static List<Map<String,Object>> executeQuery(String
sql, Object...obj) {
 Connection con = getConnection();
 ResultSet rs = null;
 PreparedStatement ps = null;
 try {
 ps = con.prepareStatement(sql);
 for (int i = 0; i < obj.length; i++) {
 ps.setObject(i + 1, obj[i]);
 }
 rs = ps.executeQuery();
 
 List<Map<String, Object>> list = new ArrayList<>();
 
 int count = rs.getMetaData().getColumnCount();
 
 while (rs.next()) {
 
 Map<String, Object> map = new HashMap<String,
Object>();
 for (int i = 0; i < count; i++) {
 Object ob = rs.getObject(i + 1);
 String key = rs.getMetaData().getColumnName(i
+ 1);
 map.put(key, ob);
 }
 list.add(map);
 }
 return list;
 } catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 } finally {
 close(rs, ps, con);
 }
 return null;
}}

 

测试:

1)可先启动⼀个进程,然后再启动⼀个进程(两个进程模拟分布式环境下,通⼀个定时任务 部署了两份在⼯作) 2)两个进程逐个启动,观察现象

3)关闭其中执⾏的进程,观察现象

Leader节点选举机制

每个Elastic-Job的任务执⾏实例App作为Zookeeper的客户端来操作ZooKeeper的znode

(1)多个实例同时创建/leader节点.

(2)/leader节点只能创建⼀个,后创建的会失败,创建成功的实例会被选为leader节点, 执⾏任务.

2.3 Elastic-Job-Lite轻量级去中⼼化的特点

  轻量级:必须依赖仅仅zookeeper,并非独立部署的中间件,就是jar程序。
  去中心化:执行节点对等(程序和jar一样,唯一不一样的可能是分片),定时调度触发(没有中心调度节点分配),服务自发现,主节点非固定。
  任务分片:⼀个⼤的⾮常耗时的作业Job,⽐如:⼀次要处理⼀亿的数据,那这⼀亿的数据存储在数据库中,如果 ⽤⼀个作业节点处理⼀亿数据要很久,在互联⽹领域是不太能接受的,互联⽹领域更希望机器的增加去 横向扩展处理能⼒。所以,ElasticJob可以把作业分为多个的task(每⼀个task就是⼀个任务分⽚),每 ⼀个task交给具体的⼀个机器实例去处理(⼀个机器实例是可以处理多个task的),但是具体每个task 执⾏什么逻辑由我们⾃⼰来指定。
  Strategy策略定义这些分⽚项怎么去分配到各个机器上去,默认是平均去分,可以定制,⽐如某⼀个机 器负载 ⽐较⾼或者预配置⽐较⾼,那么就可以写策略。分⽚和作业本身是通过⼀个注册中⼼协调的,因 为在分布式环境下,状态数据肯定集中到⼀点,才可以在分布式中沟通。
  分片配置:

JobCoreConfiguration.newBuilder("archive-job","*/2 * * * * ?", 3)
.shardingItemParameters("0=bachelor,1=master,2=doctor")
.build();
public class BackupJob implements SimpleJob {
 // 定时任务每执⾏⼀次都会执⾏如下的逻辑
 @Override
 public void execute(ShardingContext shardingContext) {
  int shardingItem =
shardingContext.getShardingItem();
  System.out.println("当前分片:" +shardingItem);
//获取分片参数
  String param = shardingContext.getShardingParameter();
  String selectSql = "select * from resume where state='未归档' and eduction = ' " + param +
"' + limit 1";
...
}

2.4 弹性扩容

  新增加⼀个运⾏实例app3,它会⾃动注册到注册中⼼,注册中⼼发现新的服务上线,注册中⼼会通知 ElasticJob 进⾏重新分⽚,那么总得分⽚项有多少,那么就可以搞多少个实例机器。

   注意:

  1)分⽚项也是⼀个JOB配置,修改配置,重新分⽚,在下⼀次定时运⾏之前会重新调⽤分⽚算法,那么 这个分⽚算法的结果就是:哪台机器运⾏哪⼀个⼀⽚,这个结果存储到zk中的,主节点会把分⽚给分好 放到注册中⼼去,然后执⾏节点从注册中⼼获取信息(执⾏节点在定时任务开启的时候获取相应的分 ⽚)。

  2)如果所有的节点挂掉值剩下⼀个节点,所有分⽚都会指向剩下的⼀个节点,这也是ElasticJob的⾼可 ⽤。

 

posted on 2022-02-12 18:43  jeolyli  阅读(458)  评论(0编辑  收藏  举报