1.概述
1.1什麼是任務調度
我們可以思考一下下面業務場景的解決方案:
-
某電商平臺需要每天上午10點,下午3點,晚上8點發放一批優惠券
-
某銀行系統需要在信用卡到期還款日的前三天進行短信提醒
-
某財務系統需要在每天凌晨0:10分結算前一天的財務數據,統計彙總
以上場景就是任務調度所需要解決的問題
任務調度是爲了自動完成特定任務,在約定的特定時刻去執行任務的過程
1.2 爲什麼需要分佈式調度
使用Spring中提供的註解@Scheduled,也能實現調度的功能
在業務類中方法中貼上這個註解,然後在啓動類上貼上@EnableScheduling
註解
@Scheduled(cron = "0/20 * * * * ? ") public void doWork(){ //doSomething }
感覺Spring給我們提供的這個註解可以完成任務調度的功能,好像已經完美解決問題了,爲什麼還需要分佈式呢?
主要有如下這幾點原因:
-
高可用:單機版的定式任務調度只能在一臺機器上運行,如果程序或者系統出現異常就會導致功能不可用。
-
防止重複執行: 在單機模式下,定時任務是沒什麼問題的。但當我們部署了多臺服務,同時又每臺服務又有定時任務時,若不進行合理的控制在同一時間,只有一個定時任務啓動執行,這時,定時執行的結果就可能存在混亂和錯誤了
-
單機處理極限:原本1分鐘內需要處理1萬個訂單,但是現在需要1分鐘內處理10萬個訂單;原來一個統計需要1小時,現在業務方需要10分鐘就統計出來。你也許會說,你也可以多線程、單機多進程處理。的確,多線程並行處理可以提高單位時間的處理效率,但是單機能力畢竟有限(主要是CPU、內存和磁盤),始終會有單機處理不過來的情況。
1.3 XXL-JOB介紹
XXL-Job:是大衆點評的分佈式任務調度平臺,是一個輕量級分佈式任務調度平臺, 其核心設計目標是開發迅速、學習簡單、輕量級、易擴展
大衆點評目前已接入XXL-JOB,該系統在內部已調度約100萬次,表現優異。
目前已有多家公司接入xxl-job,包括比較知名的大衆點評,京東,優信二手車,360金融 (360),聯想集團 (聯想),易信 (網易)等等
官網地址 https://www.xuxueli.com/xxl-job/
系統架構圖
設計思想
將調度行爲抽象形成“調度中心”公共平臺,而平臺自身並不承擔業務邏輯,“調度中心”負責發起調度請求。
將任務抽象成分散的JobHandler,交由“執行器”統一管理,“執行器”負責接收調度請求並執行對應的JobHandler中業務邏輯。
因此,“調度”和“任務”兩部分可以相互解耦,提高系統整體穩定性和擴展性;
2.快速入門
2.1 下載源碼
源碼下載地址:
https://github.com/xuxueli/xxl-job
https://gitee.com/xuxueli0323/xxl-job
2.1 初始化調度數據庫
請下載項目源碼並解壓,獲取 “調度數據庫初始化SQL腳本” 並執行即可。
“調度數據庫初始化SQL腳本” 位置爲:
/xxl-job/doc/db/tables_xxl_job.sql
2.2 編譯源碼
解壓源碼,按照maven格式將源碼導入IDE, 使用maven進行編譯即可,源碼結構如下:
2.3 配置部署調度中心
2.3.1 調度中心配置
修改xxl-job-admin
項目的配置文件application.properties
,把數據庫賬號密碼配置上
### web server.port=8080 server.servlet.context-path=/xxl-job-admin ### actuator management.server.servlet.context-path=/actuator management.health.mail.enabled=false ### resources spring.mvc.servlet.load-on-startup=0 spring.mvc.static-path-pattern=/static/** spring.resources.static-locations=classpath:/static/ ### freemarker spring.freemarker.templateLoaderPath=classpath:/templates/ spring.freemarker.suffix=.ftl spring.freemarker.charset=UTF-8 spring.freemarker.request-context-attribute=request spring.freemarker.settings.number_format=0.########## ### mybatis mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml #mybatis.type-aliases-package=com.xxl.job.admin.core.model ### xxl-job, datasource spring.datasource.url=jdbc:mysql://192.168.202.200:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=WolfCode_2017 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ### datasource-pool spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimum-idle=10 spring.datasource.hikari.maximum-pool-size=30 spring.datasource.hikari.auto-commit=true spring.datasource.hikari.idle-timeout=30000 spring.datasource.hikari.pool-name=HikariCP spring.datasource.hikari.max-lifetime=900000 spring.datasource.hikari.connection-timeout=10000 spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.hikari.validation-timeout=1000 ### xxl-job, email spring.mail.host=smtp.qq.com spring.mail.port=25 [email protected] [email protected] spring.mail.password=xxx spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory ### xxl-job, access token xxl.job.accessToken=default_token ### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en") xxl.job.i18n=zh_CN ## xxl-job, triggerpool max size xxl.job.triggerpool.fast.max=200 xxl.job.triggerpool.slow.max=100 ### xxl-job, log retention days xxl.job.logretentiondays=30
2.3.2 部署項目
運行XxlJobAdminApplication
程序即可.
調度中心訪問地址: http://localhost:8080/xxl-job-admin
默認登錄賬號 “admin/123456”, 登錄後運行界面如下圖所示。
至此“調度中心”項目已經部署成功。
2.4 配置部署執行器項目
2.4.1 添加Maven依賴
創建SpringBoot項目並且添加如下依賴:
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.3.1</version> </dependency>
2.4.2 執行器配置
在配置文件中添加如下配置:
### 調度中心部署根地址 [選填]:如調度中心集羣部署存在多個地址則用逗號分隔。執行器將會使用該地址進行"執行器心跳註冊"和"任務結果回調";爲空則關閉自動註冊;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 執行器通訊TOKEN [選填]:非空時啓用;
xxl.job.accessToken=default_token
### 執行器AppName [選填]:執行器心跳註冊分組依據;爲空則關閉自動註冊
xxl.job.executor.appname=xxl-job-executor-sample
### 執行器註冊 [選填]:優先使用該配置作爲註冊地址,爲空時使用內嵌服務 ”IP:PORT“ 作爲註冊地址。從而更靈活的支持容器類型執行器動態IP和動態映射端口問題。
xxl.job.executor.address=
### 執行器IP [選填]:默認爲空表示自動獲取IP,多網卡時可手動設置指定IP,該IP不會綁定Host僅作爲通訊實用;地址信息用於 "執行器註冊" 和 "調度中心請求並觸發任務";
xxl.job.executor.ip=127.0.0.1
### 執行器端口號 [選填]:小於等於0則自動獲取;默認端口爲9999,單機部署多個執行器時,注意要配置不同執行器端口;
xxl.job.executor.port=9999
### 執行器運行日誌文件存儲磁盤路徑 [選填] :需要對該路徑擁有讀寫權限;爲空則使用默認路徑;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 執行器日誌文件保存天數 [選填] : 過期日誌自動清理, 限制值大於等於3時生效; 否則, 如-1, 關閉自動清理功能;
xxl.job.executor.logretentiondays=30
2.4.3 添加執行器配置
創建XxlJobConfig
配置對象:
@Configuration public class XxlJobConfig { @Value("${xxl.job.admin.addresses}") private String adminAddresses; @Value("${xxl.job.accessToken}") private String accessToken; @Value("${xxl.job.executor.appname}") private String appname; @Value("${xxl.job.executor.address}") private String address; @Value("${xxl.job.executor.ip}") private String ip; @Value("${xxl.job.executor.port}") private int port; @Value("${xxl.job.executor.logpath}") private String logPath; @Value("${xxl.job.executor.logretentiondays}") private int logRetentionDays; @Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; } }
2.4.4 添加任務處理類
添加任務處理類,交給Spring容器管理,在處理方法上貼上@XxlJob
註解
@Component public class SimpleXxlJob { @XxlJob("demoJobHandler") public void demoJobHandler() throws Exception { System.out.println("執行定時任務,執行時間:"+new Date()); } }
2.5 運行HelloWorld程序
2.5.1 任務配置&觸發執行
登錄調度中心,在任務管理中新增任務,配置內容如下:
新增後界面如下:
接着啓動定時調度任務
2.5.2 查看日誌
在調度中心的調度日誌中就可以看到,任務的執行結果.
管控臺也可以看到任務的執行信息.
2.6 GLUE模式(Java)
任務以源碼方式維護在調度中心,支持通過Web IDE在線更新,實時編譯和生效,因此不需要指定JobHandler。
( “GLUE模式(Java)” 運行模式的任務實際上是一段繼承自IJobHandler的Java類代碼,它在執行器項目中運行,可使用
添加Service
@Service public class HelloService { public void methodA(){ System.out.println("執行MethodA的方法"); } public void methodB(){ System.out.println("執行MethodB的方法"); } }
添加任務配置
通過GLUE IDE在線編輯代碼
編寫內容如下:
package com.xxl.job.service.handler; import cn.wolfcode.xxljobdemo.service.HelloService; import com.xxl.job.core.handler.IJobHandler; import org.springframework.beans.factory.annotation.Autowired; public class DemoGlueJobHandler extends IJobHandler { @Autowired private HelloService helloService; @Override public void execute() throws Exception { helloService.methodA(); } }
啓動並執行程序
2.6 執行器集羣
2.6.1 集羣環境搭建
在IDEA中設置SpringBoot項目運行開啓多個集羣
啓動兩個SpringBoot程序,需要修改Tomcat端口和執行器端口
- Tomcat端口8090程序的命令行參數如下:
-Dserver.port=8090 -Dxxl.job.executor.port=9998
- Tomcat端口8090程序的命令行參數如下:
-Dserver.port=8091 -Dxxl.job.executor.port=9999
在任務管理中,修改路由策略,修改成輪詢
重新啓動,我們可以看到效果是,定時任務會在這兩臺機器中進行輪詢的執行
- 8090端口的控制檯日誌如下:
- 8091端口的控制檯日誌如下:
2.6.2 調度路由算法講解
當執行器集羣部署時,提供豐富的路由策略,包括:
-
FIRST(第一個):固定選擇第一個機器
-
LAST(最後一個):固定選擇最後一個機器;
-
ROUND(輪詢):依次的選擇在線的機器發起調度
-
RANDOM(隨機):隨機選擇在線的機器;
-
CONSISTENT_HASH(一致性HASH):
每個任務按照Hash算法固定選擇某一臺機器,且所有任務均勻散列在不同機器上。
-
LEAST_FREQUENTLY_USED(最不經常使用):使用頻率最低的機器優先被選舉;
-
LEAST_RECENTLY_USED(最近最久未使用):最久未使用的機器優先被選舉;
-
FAILOVER(故障轉移):按照順序依次進行心跳檢測,第一個心跳檢測成功的機器選定爲目標執行器併發起調度;
-
BUSYOVER(忙碌轉移):按照順序依次進行空閒檢測,第一個空閒檢測成功的機器選定爲目標執行器併發起調度;
-
SHARDING_BROADCAST(分片廣播):
廣播觸發對應集羣中所有機器執行一次任務,同時系統自動傳遞分片參數;可根據分片參數開發分片任務;
3. 分片功能講解
3.1 案例需求講解
需求:我們現在實現這樣的需求,在指定節假日,需要給平臺的所有用戶去發送祝福的短信.
3.1.1 初始化數據
在數據庫中導入xxl_job_demo.sql
數據
3.1.2 集成Druid&MyBatis
添加依賴
<!--MyBatis驅動--> <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> <!--lombok依賴--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency>
添加配置
spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8 spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.username=root spring.datasource.password=WolfCode_2017
添加實體類
@Setter @Getter public class UserMobilePlan { private Long id;//主鍵 private String username;//用戶名 private String nickname;//暱稱 private String phone;//手機號碼 private String info;//備註 }
添加Mapper處理類
@Mapper public interface UserMobilePlanMapper { @Select("select * from t_user_mobile_plan") List<UserMobilePlan> selectAll(); }
3.1.3 業務功能實現
任務處理方法實現
@XxlJob("sendMsgHandler") public void sendMsgHandler() throws Exception{ List<UserMobilePlan> userMobilePlans = userMobilePlanMapper.selectAll(); System.out.println("任務開始時間:"+new Date()+",處理任務數量:"+userMobilePlans.size()); Long startTime = System.currentTimeMillis(); userMobilePlans.forEach(item->{ try { //模擬發送短信動作 TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("任務結束時間:"+new Date()); System.out.println("任務耗時:"+(System.currentTimeMillis()-startTime)+"毫秒"); }
任務配置信息
3.2 分片概念講解
比如我們的案例中有2000+條數據,如果不採取分片形式的話,任務只會在一臺機器上執行,這樣的話需要20+秒才能執行完任務.
如果採取分片廣播的形式的話,一次任務調度將會廣播觸發對應集羣中所有執行器執行一次任務,同時系統自動傳遞分片參數;可根據分片參數開發分片任務;
獲取分片參數方式:
// 可參考Sample示例執行器中的示例任務"ShardingJobHandler"瞭解試用 int shardIndex = XxlJobHelper.getShardIndex(); int shardTotal = XxlJobHelper.getShardTotal();
通過這兩個參數,我們可以通過求模取餘的方式,分別查詢,分別執行,這樣的話就可以提高處理的速度.
之前2000+條數據只在一臺機器上執行需要20+秒才能完成任務,分片後,有兩臺機器可以共同完成2000+條數據,每臺機器處理1000+條數據,這樣的話只需要10+秒就能完成任務
3.3 案例改造成任務分片
Mapper增加查詢方法
@Mapper public interface UserMobilePlanMapper { @Select("select * from t_user_mobile_plan where mod(id,#{shardingTotal})=#{shardingIndex}") List<UserMobilePlan> selectByMod(@Param("shardingIndex") Integer shardingIndex,@Param("shardingTotal")Integer shardingTotal); @Select("select * from t_user_mobile_plan") List<UserMobilePlan> selectAll(); }
任務類方法
@XxlJob("sendMsgShardingHandler") public void sendMsgShardingHandler() throws Exception{ System.out.println("任務開始時間:"+new Date()); int shardTotal = XxlJobHelper.getShardTotal(); int shardIndex = XxlJobHelper.getShardIndex(); List<UserMobilePlan> userMobilePlans = null; if(shardTotal==1){ //如果沒有分片就直接查詢所有數據 userMobilePlans = userMobilePlanMapper.selectAll(); }else{ userMobilePlans = userMobilePlanMapper.selectByMod(shardIndex,shardTotal); } System.out.println("處理任務數量:"+userMobilePlans.size()); Long startTime = System.currentTimeMillis(); userMobilePlans.forEach(item->{ try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("任務結束時間:"+new Date()); System.out.println("任務耗時:"+(System.currentTimeMillis()-startTime)+"毫秒"); }
任務設置