Elastic-Job的基礎使用

Quartz官方分佈式方案,這種方式比較重,需要根據官方文檔新建數據表,並不推薦。
我們更常用的是使用【ElasticJob】實現分佈式任務。

Elastic job是噹噹網基於Zookepper、Quartz開發並開源的一個Java分佈式定時任務,解決了Quartz不支持分佈式的弊端。Elastic job主要的功能有支持彈性擴容,通過Zookepper集中管理和監控job,支持失效轉移等。

一、使用場景分析

大多數情況下,定時任務我們一般使用quartz開源框架就能滿足應用場景。

但如果考慮到健壯性等其它一些因素,就需要自己下點工夫,比如:要避免單點故障,至少得部署2個節點吧,但是部署多個節點,又有其它問題,有些數據在某一個時刻只能處理一次,比如 i = i+1 這些無法保證冪等的操作,run多次跟run一次,完全是不同的效果。

對於上面的問題,可以使用quartz+zk或redis分佈式鎖的解決方案

  1. 每類定時job,可以分配一個獨立的標識(比如:xxx_job)
  2. 這類job的實例,部署在多個節點上時,每個節點啓動前,向zk申請一個分佈式鎖(在xxx_job節點下)
  3. 拿到鎖的實例,才允許啓動定時任務(通過代碼控制quartz的schedule),沒拿到鎖的,處於standby狀態,一直監聽鎖的變化
  4. 如果某個節點掛了,分佈式鎖自動釋放,其它節點這時會搶到鎖,按上面的邏輯,就會從standby狀態,轉爲激活狀態,小三正式上位,繼續執行定時job。

這個方案,基本上解決了HA(High Availability))和業務正確性的問題,但是美中不足的地方有2點

  1. 無法充分利用機器性能,處於standby的節點,實際上只是一個備胎,平時啥也不幹
  2. 性能不方便擴展,比如:某個job一次要處理上千萬的數據,僅1個激活節點,要處理很久

elastic-job相當於quartz+zk的加強版,它允許對定時任務分片,可以集羣部署(每個job的"分片"會分散到各個節點上),如果某個節點掛了,該節點上的分片,會調度到其它節點上。
一般情況下,使用SimpleJob這種就可以了

二、概述

官網地址:http://elasticjob.io
Elastic-Job是一個分佈式調度解決方案,由兩個相互獨立的子項目Elastic-Job-Lite和Elastic-Job-Cloud組成。
Elastic-Job-Lite定位爲輕量級無中心化解決方案,使用jar包的形式提供分佈式任務的協調服務。

2.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結尾的數據。

2.2 分片項與業務處理解耦

Elastic-Job並不直接提供數據處理的功能,框架只會將分片項分配至各個運行中的作業服務器,開發者需要自行處理分片項與真實數據的對應關係。

2.3 個性化參數的適用場景

個性化參數即 shardingItemParameter ,可以和分片項匹配對應關係,用於將分片項的數字轉換爲更加可讀的業務代碼。

例如:按照地區水平拆分數據庫,數據庫A是北京的數據;數據庫B是上海的數據;數據庫C是廣州的數據。 如果僅按照分片項配置,開發者需要了解0表示北京;1表示上海;2表示廣州。
合理使用個性化參數可以讓代碼更可讀,如果配置爲0=北京,1=上海,2=廣州,那麼代碼中直接使用北京,上海,廣州的枚舉值即可完成分片項和業務邏輯的對應關係。

三、核心理念

3.1 分佈式調度

Elastic-Job-Lite並無作業調度中心節點,而是基於部署作業框架的程序在到達相應時間點時各自觸發調度。
註冊中心僅用於作業註冊和監控信息存儲。而主作業節點僅用於處理分片和清理等功能。

3.2 作業高可用

Elastic-Job-Lite提供最安全的方式執行作業。將分片總數設置爲1,並使用多於1臺的服務器執行作業,作業將會以1 主n從的方式執行。

一旦執行作業的服務器崩潰,等待執行的服務器將會在下次作業啓動時替補執行。開啓失效轉移功能效果更好,可以保證在本次作業執行時崩潰,備機立即啓動替補執行。

3.3 最大限度利用資源

Elastic-Job-Lite也提供最靈活的方式,最大限度的提高執行作業的吞吐量。將分片項設置爲大於服務器的數量,最好是大於服務器倍數的數量,作業將會合理的利用分佈式資源,動態的分配分片項。

例如:3臺服務器,分成10片,則分片項分配結果爲服務器A=0,1,2;服務器B=3,4,5;服務器C=6,7,8,9。 如果服務器C崩潰,則分片項分配結果爲服務器A=0,1,2,3,4;服務器B=5,6,7,8,9。在不丟失分片項的情況下,最大限度的利用現有資源提高吞吐量。

3.4 整體框架圖
  • 第一臺服務器上線觸發主服務器選舉。主服務器一旦下線,則重新觸發選舉,選舉過程中阻塞,只有主服務器選舉完成,纔會執行其他任務
  • 某作業服務器上線時會自動將服務器信息註冊到註冊中心,下線時會自動更新服務器狀態
  • 主節點選舉,服務器上下線,分片總數變更均更新重新分片標記。
  • 定時任務觸發時,如需重新分片,則通過主服務器分片,分片過程中阻塞,分片結束後纔可執行任務。如分片過程中主服務器下線,則先選舉主服務器,再分片。
  • 通過上一項說明可知,爲了維持作業運行時的穩定性,運行過程中只會標記分片狀態,不會重新分片。分片僅可能發生在下次任務觸發前。
  • 每次分片都會按服務器 IP 排序,保證分片結果不會產生較大波動
  • 實現失效轉移功能,在某臺服務器執行完畢後主動抓取未分配的分片,並且在某臺服務器下線後主動尋找可用的服務器執行任務。
框架圖
在這裏插入圖片描述

四、使用實現springBoot+ElasticJob

模擬場景
假設有三個定單數據庫(我們用三張表: pop_order_data1 , `pop_order_data2 , pop_order_data3 來模擬三個數據庫),每天晚上2點會定時把已完成訂單移到HBASE,然後刪除數據庫中的已完成訂單,這裏我們只模擬刪除訂單,而且改爲每10秒執行一次進行模擬

使用方式
elastic-Job分爲SimpleJobDataFlowJob兩種:

  • SimpleJob意爲簡單實現,未經任何封裝的類型。需實現SimpleJob接口,該接口僅提供單一方法用於覆蓋,此方法將定時執行。與Quartz原生接口相似,但提供了彈性擴縮容和分片等功能。
  • DataFlowJob用於處理數據流,需實現DataflowJob接口。該接口提供2個方法可供覆蓋,分別用於抓取(fetchData)和處理(processData)數據。會先進行一輪數據獲取,然後在處理,然後再次獲取的循環過程。

兩種都是多線程的處理機制。

4.1 導入依賴
<dependency>
	<groupId>com.dangdang</groupId>
	<artifactId>elastic-job-lite-core</artifactId>
	<version>2.1.5</version>
</dependency>
<dependency>
	<groupId>com.dangdang</groupId>
	<artifactId>elastic-job-lite-spring</artifactId>
	<version>2.1.5</version>
</dependency>
4.2 配置文件

application.properties

#端口號
server.port=7777

#配置數據源
spring.datasource.url=jdbc:mysql://localhost:3306/mydb10?serverTimezone=UTC&useSSL=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

#配置mybatis的xml文件
mybatis.mapper-locations=classpath:mappers/*.xml

bean.xml 這是spring的xml文件,裏面包含了分佈式事務的處理,這裏三個分片對應三個表操作

<?xml version="1.0" encoding="UTF-8"?>
<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">
    <!--配置作業註冊中心
        zookeeper註冊中心IP與端口列表,這裏連接的是集羣
        命名空間,一定要區別開
        base-sleep-time-milliseconds等待重試的間隔時間的初始值單位:毫秒
        max-sleep-time-milliseconds等待重試的間隔時間的最大值單位:毫秒
        max-retrie最大重試次數
    -->
    <reg:zookeeper id="elastic-job-rj"
                   server-lists="114.242.146.109:8603,114.242.146.109:8602,114.242.146.109:8601"
                   namespace="elastic-job-homework-rj"
                   base-sleep-time-milliseconds="1000"
                   max-sleep-time-milliseconds="3000"
                   max-retries="3"/>

    <!--配置作業
        註冊到的zookeeper的id
        要將自己定義的MyJob注入進去
        cron表達式,這裏是每10秒執行一次
        分片數量是3個
        個性化參數,這裏我們使用數據庫表名
    -->
    <job:simple id="simple-job-rj"
                class="com.rj.elasticjobdemo.job.MyJob"
                registry-center-ref="elastic-job-rj"
                cron="0/10 * * * * ?"
                sharding-total-count="3"
                sharding-item-parameters="0=pop_order_data1,1=pop_order_data2,2=pop_order_data3 "/>
</beans>

註冊中心配置,用於註冊和協調作業分佈式行爲的組件,目前僅支持Zookeeper

屬性名 類型 構造器注入 缺省值 描述
serverLists String 連接Zookeeper服務器的列表 包括IP地址和端口號 多個地址用逗號分隔 如:host1:2181,host2:2181
namespace String Zookeeper的命名空間
baseSleepTimeMilliseconds int 1000 等待重試的間隔時間的初始值 單位:毫秒
maxSleepTimeMilliseconds String 3000 等待重試的間隔時間的最大值 單位:毫秒
maxRetries String 3 最大重試次數
sessionTimeoutMilliseconds boolean 60000 會話超時時間 單位:毫秒
connectionTimeoutMilliseconds boolean 15000 連接超時時間 單位:毫秒
digest String 連接Zookeeper的權限令牌 缺省爲不需要權限驗證
4.3 pojo層、dao層和service層
/**
* pojo
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
    private Integer id;
    private Integer order_id;
    private Integer vender_id;
    private Integer order_state; //5表示訂單完成
}

/**
* dao
*/
public interface OrderDao {
    //添加數據
    void add_data1(Order order);
    void add_data2(Order order);
    void add_data3(Order order);

    //刪除已完成訂單(狀態5)數據
    void delete_data1(Integer id);
    void delete_data2(Integer id);
    void delete_data3(Integer id);

    //每次獲取10條訂單狀態爲已完成的數據
    List<Order> findByTen_data1();
    List<Order> findByTen_data2();
    List<Order> findByTen_data3();
}

/**
* service
*/
public interface OrderService {
    //添加數據
    void add_data1(Order order);
    void add_data2(Order order);
    void add_data3(Order order);

    //刪除已完成訂單(狀態5)數據,這裏根據job_id決定操作哪個表,id表示刪除哪個數據
    void delete_data(Integer job_id, Integer id);

    //每次獲取10條訂單狀態爲已完成的數據
    List<Order> findByTen(Integer job_id);
}
4.4 配置類
/**
 * 這個類是爲了讓springBoot將bean.xml文件解析
 */
@Configuration
@ImportResource(locations = {"classpath:bean.xml"})
public class BeanConfiguration {
}
4.5 MyJob類

這個類是Job具體要執行的操作,必須注入到前面bean.xml對應的任務中

/**
 * 使用SimpleJob必須遵從SimpleJob接口,並實現方法
 */
public class MyJob implements SimpleJob {
    @Autowired
    private OrderService orderService;

    @Override
    public void execute(ShardingContext shardingContext) {
        //格式打印
        //getShardingItem()獲取的是bean.xml文件中的分片代號
        //getShardingParameter()是自定義的個性化參數名稱
        System.out.println(String.format("Item: %s |ShardingParameter: %s | Time: %s | Thread: %s | %s",
                shardingContext.getShardingItem(),shardingContext.getShardingParameter() ,
                new SimpleDateFormat("HH:mm:ss").format(new Date()),
                Thread.currentThread().getId(), "SIMPLE"));

        //執行刪除操作
        //我們這裏從數據庫每次獲取10條狀態爲已完成訂單的數據,然後執行刪除
        List<Order> orders = orderService.findByTen(shardingContext.getShardingItem());
        for (Order order : orders) {
            orderService.delete_data(shardingContext.getShardingItem(), order.getId());
        }
        System.out.println("Thread:" + Thread.currentThread().getId() +" ,刪除了表格"+ shardingContext.getShardingParameter() +"數據條數:" + orders.size());
    }
}

運行結果如下:

Item: 0 |ShardingParameter: pop_order_data1 | Time: 20:23:40 | Thread: 67 | SIMPLE
Item: 1 |ShardingParameter: pop_order_data2 | Time: 20:23:40 | Thread: 68 | SIMPLE
Item: 2 |ShardingParameter: pop_order_data3 | Time: 20:23:40 | Thread: 69 | SIMPLE
Thread:68 ,刪除了表格pop_order_data2數據條數:10
Thread:67 ,刪除了表格pop_order_data1數據條數:10
Thread:69 ,刪除了表格pop_order_data3數據條數:10

Item: 2 |ShardingParameter: pop_order_data3 | Time: 20:23:50 | Thread: 72 | SIMPLE
Item: 0 |ShardingParameter: pop_order_data1 | Time: 20:23:50 | Thread: 70 | SIMPLE
Item: 1 |ShardingParameter: pop_order_data2 | Time: 20:23:50 | Thread: 71 | SIMPLE
Thread:70 ,刪除了表格pop_order_data1數據條數:10
Thread:71 ,刪除了表格pop_order_data2數據條數:10
Thread:72 ,刪除了表格pop_order_data3數據條數:10

Item: 0 |ShardingParameter: pop_order_data1 | Time: 20:24:00 | Thread: 73 | SIMPLE
Item: 1 |ShardingParameter: pop_order_data2 | Time: 20:24:00 | Thread: 74 | SIMPLE
Item: 2 |ShardingParameter: pop_order_data3 | Time: 20:24:00 | Thread: 75 | SIMPLE
Thread:74 ,刪除了表格pop_order_data2數據條數:10
Thread:73 ,刪除了表格pop_order_data1數據條數:10
Thread:75 ,刪除了表格pop_order_data3數據條數:10

可以看出,實現了分佈式任務,同時操作三個表進行刪除操作,且是多線程操作,速度很快。


五、單表多分片操作

除了以上的方式以外,我們還可以分片9個,每個表3個分片,這樣進行操作,但是實際應用很少。

我們同樣需要寫dao和service,這裏我們可以將表的名稱以【${}】字段形式拼接進去。

	<insert id="add_data">
        insert into ${dataName}(id,order_id,vender_id,order_state) values (#{id},#{order_id},#{vender_id},#{order_state})
    </insert>

    <delete id="delete_data">
        delete from ${dataName} where id=#{id}
    </delete>

    <select id="findByTen_data" resultType="com.rj.elasticjobdemo.pojo.Order">
        select * from ${dataName} where order_state=5 limit 10;
    </select>

bean.xml 配置僅僅修改數量和對應的名稱即可

<!--配置作業
    註冊到的zookeeper的id
    要將自己定義的MyJob注入進去
    cron表達式,這裏是每10秒執行一次
    分片數量是9個,012操作data1,345操作data2,678操作data3
    個性化參數,這裏我們使用數據庫表名
-->
<job:simple id="simple-job-rj3"
            class="com.rj.elasticjobdemo.job.MyJob"
            registry-center-ref="elastic-job-rj3"
            cron="0/10 * * * * ?"
            sharding-total-count="9"
            sharding-item-parameters="0=pop_order_data1,1=pop_order_data1,2=pop_order_data1,
                                      3=pop_order_data2,4=pop_order_data2,5=pop_order_data2,
                                      6=pop_order_data3,7=pop_order_data3,8=pop_order_data3"/>

然後就是在Job任務配置,注意【取餘】操作

@Override
public void execute(ShardingContext shardingContext) {
    //執行刪除操作
    //我們這裏從數據庫每次獲取10條狀態爲已完成訂單的數據,然後執行刪除
    List<Order> orders = orderService.findByTen_data(shardingContext.getShardingParameter());
    for (Order order : orders) {
        if (order.getId()%3==shardingContext.getShardingItem() ||
                (order.getId()%3+3)==shardingContext.getShardingItem() ||
                (order.getId()%3+6)==shardingContext.getShardingItem()) {
            orderService.delete_data(shardingContext.getShardingParameter(), order.getId());
            System.out.println("分片 " + shardingContext.getShardingParameter() + "刪除的數據id是:" + order.getId());
        }
    }
}

執行結果如下:

分片 pop_order_data1刪除的數據id是:237
分片 pop_order_data2刪除的數據id是:156
分片 pop_order_data1刪除的數據id是:238
分片 pop_order_data3刪除的數據id是:156
分片 pop_order_data3刪除的數據id是:157
分片 pop_order_data2刪除的數據id是:158
分片 pop_order_data3刪除的數據id是:155
分片 pop_order_data1刪除的數據id是:236
分片 pop_order_data2刪除的數據id是:157
分片 pop_order_data3刪除的數據id是:160
分片 pop_order_data2刪除的數據id是:159
分片 pop_order_data3刪除的數據id是:158
分片 pop_order_data1刪除的數據id是:240
分片 pop_order_data1刪除的數據id是:241
分片 pop_order_data3刪除的數據id是:159
分片 pop_order_data2刪除的數據id是:161
分片 pop_order_data1刪除的數據id是:239
分片 pop_order_data2刪除的數據id是:160

六、properties.yml配置方式以及DataFlow的job實現

這個部分實現請看【GitHub】上的代碼,模擬了數據源。
https://github.com/SquirrelMaSaKi/elastic-job-example

發佈了153 篇原創文章 · 獲贊 4 · 訪問量 5835
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章