作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!?
- 分散式任務DcsSchedule中介軟體,Github地址:https://github.com/fuzhengwei/schedule-spring-boot-starter
- 分散式任務DcsSchedule控制檯,Github地址:https://github.com/fuzhengwei/itstack-middleware-control
- 演示視訊
前言
@SpringBootApplication
@EnableScheduling
public class Application{
public static void mian(String[] args){
SpringApplication.run(Application.class,args);
}
@Scheduled(cron = "0/3 * * * * *")
public void demoTask() {
//...
}
}
咔咔,上面這段程式碼很熟悉吧,他就是SpringBoot的Schedule定時任務,簡單易用。在我們開發中如果需要做一些定時或指定時刻迴圈執行邏輯時候,基本都會使用到Schedule。
但是,如果我們的任務是比較大型的,比如;定時跑批T+1結算、商品秒殺前狀態變更、重新整理資料預熱到快取等等,這些定時任務都相同的特點;作業量大、實時性強、可用率高。而這時候如果只是單純使用Schedule就顯得不足以控制。
那麼,我們產品需求就出來了,分散式DcsSchedule任務;
- 多機器部署任務
- 統一控制中心啟停
- 當機災備,自動啟動執行
- 實時檢測任務執行資訊:部署數量、任務總量、成功次數、失敗次數、執行耗時等
嗯?有人憋半天了想說可以用Quertz,嗯可以的,但這不是本篇文章的重點。難道你不想看看一個自言開源中介軟體是怎麼誕生的嗎,怎麼推到中心Maven倉的嗎?比如下圖;真香不!
首頁監控任務列表
?好了,接下來開始介紹這個中介軟體如何使用和怎麼開發的了!
中介軟體使用
1. 版本記錄
版本 | 釋出日期 | 備註 | |
---|---|---|---|
1 | 1.0.0-RELEASE | 2019-12-07 | 基本功能實現;任務接入、分散式啟停 |
2 | 2019-12-07 | 上傳測試版本 |
2. 環境準備
- jdk1.8
- StringBoot 2.x
-
配置中心zookeeper 3.4.14 {準備好zookeeper服務,如果windows除錯可以從這裡下載:https://www-eu.apache.org/dis...}
- 下載後解壓,在bin同級路徑建立資料夾data、logs
-
修改conf/zoo.cfg,修改配置如下;
dataDir=D:\\Program Files\\apache-zookeeper-3.4.14\\data dataLogDir=D:\\Program Files\\apache-zookeeper-3.4.14\\logs
-
打包部署控制平臺
- 下載地址:https://github.com/fuzhengwei...
- 部署訪問:http://localhost:7397
3. 配置POM
<dependency>
<groupId>org.itstack.middleware</groupId>
<artifactId>schedule-spring-boot-starter</artifactId>
<version>1.0.0-RELEASE</version>
</dependency>
4. 引入分散式任務DcsSchedule @EnableDcsScheduling
- 與SpringBoot的Sceduling非常像,他的註解是;@EnableScheduling,儘可能降低使用難度
- 這個註解主要方便給我們自己的中介軟體一個入口,也是?扒拉原始碼發現的可以這麼幹{我一直說好的程式碼都很騷氣}
@SpringBootApplication
@EnableDcsScheduling
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}
5. 在任務方法上新增註解
- 這個註解也和SpringBoot的Schedule很像,但是多了desc描述和啟停初始化控制
- cron:執行計劃
- desc:任務描述
- autoStartup:預設啟動狀態
- 如果你的任務需要引數可以通過引入service去呼叫獲取等方式都可以
@Component("demoTaskThree")
public class DemoTaskThree {
@DcsScheduled(cron = "0 0 9,13 * * *", desc = "03定時任務執行測試:taskMethod01", autoStartup = false)
public void taskMethod01() {
System.out.println("03定時任務執行測試:taskMethod01");
}
@DcsScheduled(cron = "0 0/30 8-10 * * *", desc = "03定時任務執行測試:taskMethod02", autoStartup = false)
public void taskMethod02() {
System.out.println("03定時任務執行測試:taskMethod02");
}
}
6. 啟動驗證
- 啟動SpringBoot工程即可,autoStartup = true的會自動啟動任務(任務是多執行緒並行執行的)
- 啟動控制平臺:itstack-middleware-control,訪問:http://localhost:7397/ 成功介面如下;可以開啟/關閉驗證了!{功能還在完善}
中介軟體開發
以SpringBoot為基礎開發一款中介軟體我也是第一次,因為接觸SpringBoot也剛剛1個月左右。雖然SpringBoot已經出來挺久的了,但由於我們專案開發並不使用SpringBoot的一套東西,所以一直依賴沒有接觸。直到上個月開始考慮領域驅動設計才接觸,嗯!真的不錯,那麼就開始了夯實技能、學習思想用到專案裡。
按照我的產品需求,開發這麼一款分散式任務的中介軟體,我腦袋中的模型已經存在了。另外就是需要開發過程中去探索我需要的知識工具,簡單包括;
- 讀取Yml自定義配置
- 使用zookeeper作為配置中心,這樣如果有機器當機了就可以通過臨時節點監聽知道
- 通過Spring類;ApplicationContextAware, BeanPostProcessor, ApplicationListener,執行服務啟動、註解掃描、節點掛在
- 分散式任務統一控制檯,來管理任務
1. 工程模型
schedule-spring-boot-starter
└── src
├── main
│ ├── java
│ │ └── org.itstack.middleware.schedule
│ │ ├── annotation
│ │ │ ├── DcsScheduled.java
│ │ │ └── EnableDcsScheduling.java
│ │ ├── annotation
│ │ │ └── InstructStatus.java
│ │ ├── config
│ │ │ ├── DcsSchedulingConfiguration.java
│ │ │ ├── StarterAutoConfig.java
│ │ │ └── StarterServiceProperties.java
│ │ ├── domain
│ │ │ ├── DataCollect.java
│ │ │ ├── DcsScheduleInfo.java
│ │ │ ├── DcsServerNode.java
│ │ │ ├── ExecOrder.java
│ │ │ └── Instruct.java
│ │ ├── export
│ │ │ └── DcsScheduleResource.java
│ │ ├── service
│ │ │ ├── HeartbeatService.java
│ │ │ └── ZkCuratorServer.java
│ │ ├── task
│ │ │ ├── TaskScheduler.java
│ │ │ ├── ScheduledTask.java
│ │ │ ├── SchedulingConfig.java
│ │ │ └── SchedulingRunnable.java
│ │ ├── util
│ │ │ └── StrUtil.java
│ │ └── DoJoinPoint.java
│ └── resources
│ └── META_INF
│ └── spring.factories
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
2. 程式碼講解
- 篇幅較長,只講解部分重點程式碼塊,如果你願意參與到開源編寫,可以和我申請
- 我說過好的程式碼都很騷氣,那麼就從這部分入手吧
2.1 自定義註解
annotation/EnableDcsScheduling.java & 自定義註解
這個註解一堆的圈A,這些配置都是為了開始啟動執行我們的中介軟體;
- Target 標識需要放到類上執行
- Retention 註釋將由編譯器記錄在類檔案中,並且在執行時由VM保留,因此可以反射地讀取它們
- Import 引入入口資源,在程式啟動時會執行到自己定義的類中,以方便我們;初始化配置/服務、啟動任務、掛在節點
- ComponentScan 告訴程式掃描位置
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({DcsSchedulingConfiguration.class})
@ImportAutoConfiguration({SchedulingConfig.class, CronTaskRegister.class, DoJoinPoint.class})
@ComponentScan("org.itstack.middleware.*")
public @interface EnableDcsScheduling {
}
2.2 掃描自定義註解、初始化配置/服務、啟動任務、掛在節點
config/DcsSchedulingConfiguration.java & 初始化配置/服務、啟動任務、掛在節點
- 寫到這的時候,我們的自定義註解有了,已經寫到方法上了,那麼我們怎麼拿到呢?
- 需要通過實現BeanPostProcessor.postProcessAfterInitialization,在每個bean例項化的時候進行掃描
- 這裡遇到一個有趣的問題,一個方法會得到兩次,因為有一個CGLIB給代理的,像真假美猴王一樣,幾乎一毛一樣。?扒了原始碼才看到,生命註解批註沒有。好那就可以判斷了!method.getDeclaredAnnotations()
- 我們將掃描下來的任務資訊彙總到Map中,當Spring初始化完成後,在執行我們中介軟體內容。{太早執行有點喧賓奪主了!主要人家也不讓呀,給你拋異常?。}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (this.nonAnnotatedClasses.contains(targetClass)) return bean;
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
if (methods == null) return bean;
for (Method method : methods) {
DcsScheduled dcsScheduled = AnnotationUtils.findAnnotation(method, DcsScheduled.class);
if (null == dcsScheduled || 0 == method.getDeclaredAnnotations().length) continue;
List<ExecOrder> execOrderList = Constants.execOrderMap.computeIfAbsent(beanName, k -> new ArrayList<>());
ExecOrder execOrder = new ExecOrder();
execOrder.setBean(bean);
execOrder.setBeanName(beanName);
execOrder.setMethodName(method.getName());
execOrder.setDesc(dcsScheduled.desc());
execOrder.setCron(dcsScheduled.cron());
execOrder.setAutoStartup(dcsScheduled.autoStartup());
execOrderList.add(execOrder);
this.nonAnnotatedClasses.add(targetClass);
}
return bean;
}
- 初始化服務連線zookeeper配置中心
- 連線後將建立我們的節點以及新增監聽,這個監聽主要負責分散式訊息通知,收到通知負責控制任務啟停
- 這裡包括了迴圈建立節點以及批量節點刪除,似乎!面試題會問?
private void init_server(ApplicationContext applicationContext) {
try {
//獲取zk連線
CuratorFramework client = ZkCuratorServer.getClient(Constants.Global.zkAddress);
//節點組裝
path_root_server = StrUtil.joinStr(path_root, LINE, "server", LINE, schedulerServerId);
path_root_server_ip = StrUtil.joinStr(path_root_server, LINE, "ip", LINE, Constants.Global.ip);
//建立節點&遞迴刪除本服務IP下的舊內容
ZkCuratorServer.deletingChildrenIfNeeded(client, path_root_server_ip);
ZkCuratorServer.createNode(client, path_root_server_ip);
ZkCuratorServer.setData(client, path_root_server, schedulerServerName);
//新增節點&監聽
ZkCuratorServer.createNodeSimple(client, Constants.Global.path_root_exec);
ZkCuratorServer.addTreeCacheListener(applicationContext, client, Constants.Global.path_root_exec);
} catch (Exception e) {
logger.error("itstack middleware schedule init server error!", e);
throw new RuntimeException(e);
}
}
- 啟動標記了True的Schedule任務
- Scheduled預設是單執行緒執行的,這裡擴充套件為多執行緒並行執行
private void init_task(ApplicationContext applicationContext) {
CronTaskRegister cronTaskRegistrar = applicationContext.getBean("itstack-middlware-schedule-cronTaskRegister", CronTaskRegister.class);
Set<String> beanNames = Constants.execOrderMap.keySet();
for (String beanName : beanNames) {
List<ExecOrder> execOrderList = Constants.execOrderMap.get(beanName);
for (ExecOrder execOrder : execOrderList) {
if (!execOrder.getAutoStartup()) continue;
SchedulingRunnable task = new SchedulingRunnable(execOrder.getBean(), execOrder.getBeanName(), execOrder.getMethodName());
cronTaskRegistrar.addCronTask(task, execOrder.getCron());
}
}
}
- 掛在任務節點到zookeeper掛在
- 按照不同的場景,有些內容是掛在到虛擬機器節點。{?又來個面試題,虛擬節點資料怎麼掛在,建立的是永久節點,那麼虛擬值怎麼加?}
- path_root_server_ip_clazz_method;這個結構是:根目錄、服務、IP、類、方法
private void init_node() throws Exception {
Set<String> beanNames = Constants.execOrderMap.keySet();
for (String beanName : beanNames) {
List<ExecOrder> execOrderList = Constants.execOrderMap.get(beanName);
for (ExecOrder execOrder : execOrderList) {
String path_root_server_ip_clazz = StrUtil.joinStr(path_root_server_ip, LINE, "clazz", LINE, execOrder.getBeanName());
String path_root_server_ip_clazz_method = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName());
String path_root_server_ip_clazz_method_status = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName(), "/status");
//新增節點
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz);
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz_method);
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz_method_status);
//新增節點資料[臨時]
ZkCuratorServer.appendPersistentData(client, path_root_server_ip_clazz_method + "/value", JSON.toJSONString(execOrder));
//新增節點資料[永久]
ZkCuratorServer.setData(client, path_root_server_ip_clazz_method_status, execOrder.getAutoStartup() ? "1" : "0");
}
}
}
2.3 zookeeper控制服務
service/ZkCuratorServer.java & zk服務
- 這裡提供一個zk的方法集合,其中比較重要的方法新增監聽
- zookeeper有一個特性是對這個監聽後,當節點內容發生變化時會收到通知,當然當機也是收得到的,這個也就是我們後面開發災備的核心觸發點
public static void addTreeCacheListener(final ApplicationContext applicationContext, final CuratorFramework client, String path) throws Exception {
TreeCache treeCache = new TreeCache(client, path);
treeCache.start();
treeCache.getListenable().addListener((curatorFramework, event) -> {
//...
switch (event.getType()) {
case NODE_ADDED:
case NODE_UPDATED:
if (Constants.Global.ip.equals(instruct.getIp()) && Constants.Global.schedulerServerId.equals(instruct.getSchedulerServerId())) {
//執行命令
Integer status = instruct.getStatus();
switch (status) {
case 0: //停止任務
cronTaskRegistrar.removeCronTask(instruct.getBeanName() + "_" + instruct.getMethodName());
setData(client, path_root_server_ip_clazz_method_status, "0");
logger.info("itstack middleware schedule task stop {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
case 1: //啟動任務
cronTaskRegistrar.addCronTask(new SchedulingRunnable(scheduleBean, instruct.getBeanName(), instruct.getMethodName()), instruct.getCron());
setData(client, path_root_server_ip_clazz_method_status, "1");
logger.info("itstack middleware schedule task start {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
case 2: //重新整理任務
cronTaskRegistrar.removeCronTask(instruct.getBeanName() + "_" + instruct.getMethodName());
cronTaskRegistrar.addCronTask(new SchedulingRunnable(scheduleBean, instruct.getBeanName(), instruct.getMethodName()), instruct.getCron());
setData(client, path_root_server_ip_clazz_method_status, "1");
logger.info("itstack middleware schedule task refresh {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
}
}
break;
case NODE_REMOVED:
break;
default:
break;
}
});
}
2.4 並行任務註冊
- 由於預設的SpringBoot是單執行緒的,所以這裡改造了下,可以支援多執行緒並行執行
- 包括了新增任務和刪除任務,也就是執行取消future.cancel(true)
public void addCronTask(SchedulingRunnable task, String cronExpression) {
if (null != Constants.scheduledTasks.get(task.taskId())) {
removeCronTask(task.taskId());
}
CronTask cronTask = new CronTask(task, cronExpression);
Constants.scheduledTasks.put(task.taskId(), scheduleCronTask(cronTask));
}
public void removeCronTask(String taskId) {
ScheduledTask scheduledTask = Constants.scheduledTasks.remove(taskId);
if (scheduledTask == null) return;
scheduledTask.cancel();
}
2.5 待擴充套件的自定義AOP
- 我們最開始配置的掃描@ComponentScan("org.itstack.middleware.*"),主要用到這裡的自定義註解,否則是掃描不到的,也就是你自定義切面失效的效果
- 目前這裡的功能並沒有擴充套件,基本只是列印執行耗時,後續完善的任務執行耗時監聽等,就需要這裡來完善
@Pointcut("@annotation(org.itstack.middleware.schedule.annotation.DcsScheduled)")
public void aopPoint() {
}
@Around("aopPoint()")
public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
long begin = System.currentTimeMillis();
Method method = getMethod(jp);
try {
return jp.proceed();
} finally {
long end = System.currentTimeMillis();
logger.info("\nitstack middleware schedule method:{}.{} take time(m):{}", jp.getTarget().getClass().getSimpleName(), method.getName(), (end - begin));
}
}
3. Jar包釋出
開發完成後還是需要將Jar包釋出到manven中心倉庫的,這個過程較長單獨寫了部落格;釋出Jar包到Maven中央倉庫(為開發開源中介軟體做準備)
綜上總結
- 要開發要實現的還很多,一個週末也幹不完所有的!而且需要有想法的小猿/媛伴一起加入!? ? ?
- 這裡沒有講解分散式任務中介軟體控制平臺itstack-middleware-control,因為比較簡單只是使用了中介軟體的zk功能介面做展示和操作。
- 中介軟體開發是一件非常有意思的事情,不同於業務它更像易筋經,寺廟老僧,劍走偏鋒,馳騁縱橫,騷招滿屏。