目录
1.背景
项目中需要对多个基站进行管理,根据任务的执行时间情况,决定将耗时的任务统一改为异步调用,同时由于基站任务执行的特殊性,还需要针对需要到达基站的任务进行串行编排,保证一个基站同一时刻只有一个任务在执行,对于异步任务的提交、执行情况通过WebSocket消息机制及时通知前端用户。同时为了保证用户体验,针对不耗时的任务依旧采用同步方式调用。
2.总体设计
2.1设计思想
2.1.1 任务的多级缓存机制
- 一级缓存:界面所有提交的关于基站的原始任务,将先存入Redis原始任务消息队列中,形成关于任务的一级缓存,通过任务调度服务的编排功能实现任务的下发。
- 二级缓存:任务调度服务根据“单基站任务串行调度规则”将符合调度下发条件的任务,从原始任务消息队列中移动到待执行任务队列中
- 三级缓存(线程池缓存):当被提交到执行器线程池的任务超过线程池核心线程数但为达到最大线程池数量时,任务将会被放入线程池任务队列中,形成任务缓存。
2.1.2调度与执行分离
将调度行为抽象形成“任务调度器’,而调度器本身并不承担具体任务业务逻辑执行,只负责发起调度请求。“执行器”采用“池化”思想管理执行线程,而每个执行线程负责具体的任务业务逻辑执行及结果反馈,同时具备对执行失败的任务发起重试,在重试次数超过阈值后发出告警信息。
2.1.3 单基站任务编排及多基站并行处理
(一)单基站任务编排:主要是为了解决针对单基站瞬时任务冲突问题,任务冲突可能发生的情况如下:
1)定时任务和定时任务冲突;
2)定时任务和手动任务冲突;
3)手动任务和手动任务冲突。
(二)多基站并行处理:通过使用线程池减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,从而避免了相应的资源消耗。同时对于针对多基站的批量任务,可并行执行多个任务,最大限度的利用CPU。
2.1.4 任务提交和任务调度线程间阻塞调度机制
在任务提交线程(AcsTaskManager)和任务调度器线程(AcsTaskDispatcher)两个线程之间加入阻塞唤醒机制。实现“任务调度线程如果发现任务队列为空则进行wait(等待被唤醒),任务提交线程AcsTaskManager提交任务时会唤醒处于等待的任务调度线程”,从而避免任务调度器线程因为轮询机制带来的CPU空转。
2.1.5 任务失败重试(后续实现)
支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
2.1.6 任务失败告警(后续实现)
1)(现阶段)任务失败时发布告警信息并写入告警数据表,同现有告警模块联动,支持在界面上直接查看告警信息。
2)(后续规划)可通过接入第三方服务平台,提供邮件、短信等方式失败告警;
2.2 模块设计
系统整体分为如下几个模块:
- 任务缓存模块:任务相关缓存(包括正在执行中的任务映射表、异步任务缓存队列、任务执行失败映射表)。
- 任务调度线程:负责从缓存中提取任务数据,如果当前任务对应的基站当前没有执行中的任务则进行任务的下发,否则不处理。
- 任务执行引擎:通过引入线程池实现多任务的并行执行,所有任务以Future任务方式执行以便获取执行结果。
- 任务执行结果解析引擎:对请求结果数据进行统一解析处理,同时根据请求结果对缓存及DB中的数据进行更新删除等操作。
- 统一HTTP客户端框架:能够通过调用本地接口方法的方式发送 HTTP 请求。
整体流程图如下:
3.实现
下面针对框架中的一些关键模块及流程进行说明:
3.1统一HTTP客户端框架
系统使用开源的Forest作为HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。Forest 会将定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。Spring Boot项目通过如下方式引入使用即可:(新手介绍 | Forest)
<groupId>com.dtflys.forest</groupId>
<artifactId>spring-boot-starter-forest</artifactId>
<version>1.4.10</version>
</dependency>
通过定义类似如下接口来统一实现并支持POST、GET、DELETE、PUT常见四种类型的请求:
import java.util.Map;
import com.dtflys.forest.annotation.BaseRequest;
import com.dtflys.forest.annotation.Body;
import com.dtflys.forest.annotation.Delete;
import com.dtflys.forest.annotation.Get;
import com.dtflys.forest.annotation.Post;
import com.dtflys.forest.annotation.Put;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
@BaseRequest(
baseURL = "${baseUrl}",
headers = {
"Content-Type: application/json"
}
)
public interface AcsClientNew {
@Post(url = "${0}")
HttpclientForestResponse execPost(String url, @Body Map<String, Object> bodyMap);
@Get(url = "${0}")
HttpclientForestResponse execGet(String url);
@Delete(url = "${0}")
HttpclientForestResponse execDelete(String url);
@Put(url = "${0}")
HttpclientForestResponse execPut(String url, @Body Map<String, Object> bodyMap);
}
其他业务模块,通过注解引入并进行调用:
@Autowired
AcsClientNew acsClientNew;
需要说明的是,这里为了保证调用的灵活性,在配置中心定义好baseUrl(例如:http://127.0.0.1/),然后通过方法参数的url字段指定具体需要调用的接口(例如:test/getUser)。
3.2异步任务缓存机制
3.2.1 关键元素
(一) taskQueue (任务队列)
- 作用:缓存业务层调用方提交的异步任务。
- 实现:使用CopyOnWriteArrayList容器来进行任务的存储,之所以选择CopyOnWriteArrayList主要是基于两方面考虑:1)当前任务队列是读多写少的场景;2)避免通过使用锁机制来实现保证读写分,保证并发安全。
(二)curExecutingTaskMap(当前有任务执行的基站映射表)
- 作用:记录当前正在执行任务的映射表,key为基站Id 或 基站Id_EXPIRE,value为任务实体或者过期时间。
- 实现:这里使用同一个Map来存储如下两种类型数据:
- 基站ID -> 存储任务 (eg: key= FFFFFF-11111 , value=AcsTask实例)
- 基站ID_EXPIRE -> 过期时间 (eg: key=FFFFFF-11111_EXPIRE,value=3000)
- 对于过期时间的说明:针对单个基站,如果因为某些原因导致之前发送过去的任务一直未能响应或执行,则会导致curExecutingTaskMap中一直存在“正在执行中的”任务,然后直接导致后续其他操作任务无法下发。
(三)failureTimesMap (任务失败次数映射表)
- 作用:记录当前正在执行任务失败次数的映射表,当某个任务超过最大执行失败次数阈值(默认为3次)则该任务将会从任务队中删除,同时清理掉正在执行任务的映射表数据,防止因为某个任务无法执行而导致后续针对同一基站的任务无法下发并执行。
- 实现:这里使用同一个Map来存储如下两种类型数据:
- 任务ID -> 失败次数 (eg: key= FFFFFF-11111 , value=AcsTask实例)
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.bonc.omc.domain.AcsTask;
/**
*
* @ClassName: AcsTaskCache
* @Description: AcsTask 任务相关缓存(包括正在执行中的任务映射表、任务缓存队列)
* @Author: liulianglin
* @DateTime 2022年1月19日 下午5:14:47
*/
@Component
public final class AcsTaskCache {
private static final Logger log = LoggerFactory.getLogger(AcsTaskCache.class);
private Object sleepLock;
/**
* 每个缓存最大存活时间3分钟
*/
private static final Integer MA