這是我第一次做java技術比較全面和複雜的系統,當時剛從事互聯網開發,它與傳統單機增刪改查的Web應用差別很大,那只是業務複雜。當時除了學習很多工具技巧外包括maven/git使用都才入門,線程處理的相關技術整合使用還不多,自己一個人一下做這個還是蠻有壓力的,新的工作需要打響第一炮。這時我只看過一部分dubbo與druid源碼,有了些想法,最後一步步也順利完成了。
開發一個系統其實就是組織一個工廠,安排各種人員,各司其職又相互協作,處理業務的過程。不過後來又看了些源碼,比如rocketmq,發現有些相似的思想,也有更多設計顯的稚嫩。現在看的多了,更可以參考優秀的設計,無法結構/代碼/細節功能都可以向高水平看齊。
本系統的調查中心有兩個核心類,一個是子任務應用註冊管理類,類比註冊中心,一個是消息處理容器類,內部包括線程池,內部消息處理,重試,持久化,監聽連接,選擇子任務應用策略類等。
兩年前做的系統,有時都想不起來細節的設計了,所以打算總結一下,隨便分析一下可以改進的地方。
1. 系統的功能與特點
1.1 總體項目概述
公司已經有一個徵信產品,是兩個單體架構組成,其中一個web應用,將徵信請求進入redis隊列,另一個單體應用負責從redis中取出請求,進行相關的處理,並將結果寫入mongoDb,web應用會輪詢結果顯示。
本次開發的背調產品,經過總監,CTO,架構師,及我們開發組長們討論後,相關的思考與決定如下:
- SOA架構:爲了支撐大併發,高流量的互聯網應用,應用拆分爲多個細粒度服務,服務要求無狀態,接口實現冪等。包括企業服務(背調發起與查詢),用戶服務(簡歷與應聘記錄),產品服務(背調產品,單次,計次,包月等,單項有按次,按結果收費,是否退費),訂單服務,調查服務,支付服務,短信服務等。爲了快速搭建整個系統,服務之間通過restful調用,自研通訊組件與協議架構師完善並在我開發的調查中使用。我喜歡用流行的,比較新的技術,但我們提出的使用dubbo沒被同意,後來在渠道推廣模塊使用了。
- 服務庫:各服務負責自己的數據庫,基於mycat的讀寫分離。訂單是最核心的數據,企業是主要的服務對象,用戶這邊弱化了,畢竟招聘不是重點,可能只操作背調授權。所以服務企業纔是重點,訂單按企業id分片,僅冗餘訂單主要信息供其它維度查詢,訂單id包含企業id,所以詳情都走分片查詢。
- 緩存中間件:共用徵集的redis緩存,使用了redis的哨兵模式。主要是存放登錄相關,如token;存放表級數據,比如用戶,產品等數據;存放接口調用緩存,比如最多的查詢企業訂單列表;存放未完成訂單。
- 消息中間件:有利於應對高併發,有利於多點訂閱或者不確定的消費方,也可以延時處理。但暫時沒有人手可靠搭建,所以沒有使用。於是在調查串行的重點應用中前端要進行限流和有專門線程處理問題訂單。
1.2 我負責的業務
企業服務平臺中選擇調查產品,錄入或者選擇投遞的調查人信息,發起調查開始的所有相關業務。主要有生成訂單,扣次數或者扣費後,發起授權。被調查人授權後,提交調查系統處理,根據調查結果進行費用調整。前臺輪詢處理結果。由於有預付,有授權,所以訂單可能會停留在未支付,未授權,調查中等中間狀態,其它連續動作由TCC分佈式事務保證。另外還有安排相關人員進行訂單服務、子調查系統開發,後來又負責企業服務。
本文介紹的只是其中最複雜的調查系統處理。特點:
- 類似消息中間件,使用公司自己的通訊組件與協議。
- 要實現處理子調查應用的註冊,心跳,狀態上報,監聽,重試,選擇策略。
- 實現調查消息的持久化、分發,消費確認過程。
- 同時要前端限流,以及系統自身的維護線程。
- 子應用的處理與狀態上報,分有離線人工調查與自動調查。
1.3 本系統介紹
這個系統有點類似於消息中間件的開發,調查服務端(以下簡稱 SS)接收的消息是一個個背調請求,系統要持久化,每個請求要再要分解出一個個單項調查子任務,推送到各個調查客戶端(以下簡稱 SC)進行消費,及時的調查返回結果,人工調查 SC只返回收到任務。
比如用戶選擇調查一個人的身份/學歷/刑事處罰/不良信息。SS接收這個任務,拆解後分別發送給處理身份,學歷,刑事處罰,不良信息的四類SC,每類可能不只一個應用,也許有兩個調用不良信息的SC都可以完成單獨的任務。這個系統有以下幾個特點:
- SS與SC之間通過公司自己開發的基於netty的通訊組件進行,我參與了這個組件開發,在分析通訊協議設計的文章中介紹了我們的通訊組件。rocketmq中也是自己的通訊組件,自己的專用協議。
- 這個過程中還要維護調查客戶端的連接,狀態,同類的客戶端要進行選擇性的推送。這個類似於nameServer的功能,有心跳功能,要監聽通訊通道的狀態,我們集成在SS中,更沒有實現高可用。
- SS要嵌入到一個web應用中,接收企業服務應用過來的http背調請求,未來內部使用還考慮也支持自己的remote組件,比如相應的企業服務中可以引用背調請求的生產者。當然對外部推廣客戶還是要http的方式的跨防火牆請求。
- 每一個SC也會嵌入一個web應用中,可以查看接收的單項調查子任務的情況。SC要容納各種子任務的實現,通過配置化只加載其中的一種。SC只處理push過來的子任務,沒實現主動pull。
- SS收到的消息要支持持久化,提供相應的接口,可以配置具體的方式。
- 異步處理爲主。比如SC完成的結果後通知SS,SS有監聽器異步監聽完成情況,進行更新。
- 後期由於提出了人工干預同類SC的處理,以及有人工電話調查SC並準備使用dubbo技術,所以又改造了一下,增加了中間處理層(以下簡稱SM),同類的SC註冊到SM中,而SM又註冊到SS中。SM也有局部的nameServer的功能,也有業務功能。
2. 項目的組成
項目由一個總的pom工程,和三個jar工程組成,分別是survey-client,survey-middle,survey-server組成。
下面以survey-server爲主,其它兩個模塊簡單介紹。
3. 服務端的設計
3.1 總體設計
服務端有兩大功能,分別是處理背調請求,維護客戶端的狀態。有點類似與rocketmq的broker與nameSvr的兩大功能。
這兩個功能都有核心類,因爲因爲附屬功能都在其中,所以我當時喜歡叫Container,也許現在會學rocketmq叫controller了。兩個核心類不需要遠程通訊,但它們相互引用。
TaskContainer管理任務,AppContainer管理註冊的客戶端,它們由一個inti類進行統一啓動兩個核心類,以及注入持久化實現類。同時init類還會啓動一個守護進程,輸出一些核心類中的重要日誌信息給控制檯。
3.2 通訊中間件使用
基於netty開發。
服務端事先配置可以連接的客戶端的信息,包括appKey,code,appSecret等信息。如果客戶端連接成功了,會有一個sessionId,由服務端保存。其內部有驗證,重連等機制。
通訊層本身有連接心跳功能,但上層還需要一個業務心跳,傳遞業務執行情況與服務器狀態變化 ,上層選擇客戶端時就可以基於多種策略了。
3.2.1 服務端發消息與處理
服務端的啓動:
//服務端啓動,包括端口以及可允許連接的客戶端信息。這些信息用於底層校驗客戶端
ServerInit.init(serverPort, clientAppList);
服務端push消息給客戶端:
一般通訊層服務端會返回連接好的channel給使用者,使用者可以包裝成自己的channel進行使用。這裏並不提供channel,其內部持有。外部使用如下方式:
//ServerPushHandler是通訊層的類,有靜態方法發信息給客戶端。
//根據客戶端的sessionId發送一個【任務(名稱)】以及數據,並設置好返回結果的回調對象來異步處理返回值。
//當然也可以當成dispatcher來用。
boolean bln = ServerPushHandler.pushBySessionId(subTaskInfo.getSurveyClient().getSessionId(), "assignTaskToClient", body, new ServerPushTaskCallback());
客戶端會註冊對應【任務(名稱)】的處理類,來處理接收到的數據並返回值。
//客戶端配置對應的處理類。
PushReceiverCenter.registReceiver("assignTaskToClient", new PushReceiverFeedbackHandler(apiInvorkerInterface));
3.2.2 客戶端發消息給與服務端處理
客戶端啓動:
//設置狀態監聽類。底層netty監控到狀態變化會通知這個類對象來處理
client = ClientCenter.getAClient(this.serverIP, serverPort, this.appkey, this.appSecret,clientStatusListener);
發送與接收處理:
//客戶端發送業務心跳數據的方式,消息本身包含【消息名稱】
//同時還設置了服務端返回值的處理類,確認服務端已經收到了。
MiddleMsg msg = new MiddleMsg("getClientSystemInfo", body);
ResultObject res = client.sendMsg(msg, new ReportClientInfoFeedbackHandler());
//服務端註冊消息的處理類,根據【消息名稱】,選擇對應的處理類
register.addMsgServiceHandler("getClientSystemInfo",RecvClientInfoHandler);
3.2.3 設計改進
在rocketmq中,通訊層與核心類不直接引用,中間有一個outApi的類隔離着,對核心類與其它類提供所有的通訊功能服務。
我的設計中,通訊層屬於AppContainer,直接使用了,考慮整體通訊的統一性,應該被一個獨立的類被引用使用,獨立的類負責通訊,註冊消息處理類等功能。
3.2 客戶端管理核心類的功能
3.2.1 屬性
客戶端管理類,主要有配置的客戶端與在線的客戶端。由於由服務端與客戶端兩層改爲三層,並且原始的配置值被意義被要求改變,比如appKey由一個客戶端變成了一類客戶端。還涉及到客戶端配置管理服務不能及時變化造成的一定冗餘。有些傳參被要求變動,還涉及到通訊層的參數變動,所以屬性中有些意義發生變化。
ClientApp是通訊層要的配置客戶端類,SurveyApp是本業務中的客戶端。
public class AppContainer {
private static final Logger logger =Logger.getLogger(AppContainer.class);
private String serverIp;
private String serverPort;
/**用戶單例鎖*/
private static Boolean lockSigleton = true;
private static AppContainer appContainer;//單例使用
/**配置的app,由於底層沒有code,用appKey記錄,所以監聽底層的Client用AppKey確定。
//【appKey--(SurveyApp--SurveyClientList)】*/
public Map<String,SurveyApp> appHolder=new HashMap<String,SurveyApp>();
/**配置的app,任務用Code記錄(在任務處理中,因爲子任務中使用AppCode指明使用的App類型,這是從產品那邊配置的,不太可能用AppKey這個不是很清晰的碼)
* 【appCode--(SurveyApp--SurveyClientList)】
*/
public Map<String,SurveyApp> appCodeHolder=new HashMap<String,SurveyApp>();
/**配置的Client(ClientCode--SurveyClient)*/
public Map<String,SurveyClient> clientHolder=new HashMap<String,SurveyClient>();
/**可維護的Client在線列表。
* <key爲appKey(代表一類客戶端),內部一組客戶端,有併發控制<sessionId,client>>
【appKey--(sessionId--onlineClient)】
*/
public volatile Map<String, ConcurrentHashMap<String, SurveyClient>> onlineClientMap=new HashMap<String, ConcurrentHashMap<String,SurveyClient>>();
/**根據配置的SurveyClient,生成ClientApp列表,僅用於啓動中間件前給底層中間件傳遞*/
private List<ClientApp> clientAppList=new ArrayList<ClientApp>();
private TaskContainer taskContainer;//引用任務處理容器
public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public boolean isMiddlewearStarted=false;
3.2.2 主要方法
- 啓動通訊服務端,註冊三個業務處理類:客戶端狀態監聽,處理客戶端業務心跳,處理客戶端完成任務情況上報處理。
public void initMiddlerWareStart(){
logger.info("【背調中心】註冊客戶心跳消息與任務完成消息的回調處理");
try {
// 獲取應用client基本信息
MsgServiceHandlerRegister register = MsgServiceHandlerRegister.getRegister();
//註冊事件處理類
MsgServiceHandlerRegister.setEventHandlerClass(ClientStatusListener.class);
register.addMsgServiceHandler("getClientSystemInfo",RecvClientInfoHandler.class);
register.addMsgServiceHandler("subTaskFinishInfo", RecvClientTaskHandler.class);
new Thread(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
try {
isMiddlewearStarted=true;
ServerInit.init(StringUtils.isEmpty(serverPort)?9166:Integer.parseInt(serverPort), clientAppList);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
isMiddlewearStarted=false;
}
}
}).start();
logger.debug("【背調中心】啓動消息服務成功!端口:" + serverPort);
} catch (Exception e) {
isMiddlewearStarted=false;
e.printStackTrace();
logger.error("【背調中心】啓動消息服務失敗!異常:" + e.toString());
}
}
- ClientStatusListener主要監聽客戶端登錄成功事件,以及客戶端斷開連接事件。登錄成功將產生一個surveyClient,並以sessionId爲key存在map中。surveyClient還有一部分業務信息,比如權重等,來自業務心跳給補充。
//public class ClientStatusListener extends AbstractEventHandler中的方法。
//將產生一個surveyClient客戶端。
@Override
public void loginSuccess(EventInfo res) {
// TODO Auto-generated method stub
super.loginSuccess(res);
ContainerInit containerInit = ContainerInit.getInstance();
if (containerInit != null) {
ClientApp clientApp = res.getAppinfo();
Map<String, SurveyClient> surveyClientMap = AppContainer.instance.onlineClientMap.get(clientApp.getAppKey());
if (surveyClientMap == null)
surveyClientMap = new ConcurrentHashMap<String, SurveyClient>();
SurveyApp surveyApp = AppContainer.appHolder.get(clientApp.getAppKey());
// 1.新建一個調查客戶端,以sessionId爲key,記錄在app表下面。
// surveyClient中一部分來源上監聽,另一部分信息要來源於業務心跳。
SurveyClient surveyClient = new SurveyClient(clientApp.getIp(), "", clientApp.getSessionId(), clientApp.getChannelId(), surveyApp);
surveyClientMap.put(clientApp.getSessionId(), surveyClient);
// 2.新建一個準備放置客戶端下的子任務。
TaskContainer.clientSubTaskInfoMap.put(clientApp.getSessionId(), new ArrayList<SubTaskInfo>());// 新建此客戶端下的子任務容器
logger.info("【中間件狀態監聽】此APP當前客戶端總數:" + surveyClientMap.size());
}
}
//public class RecvClientInfoHandler implements MsgServiceHandler中的方法。
//將對surveyClient客戶端信息進行補充。包括客戶端的類型code,權重,更新時間等,未來增加其它性能數據。
@Override
public MiddleMsg handleMsgEvent(MsgEvent dm, MiddleMsg msg) {
String body = msg.getBody() + "";
String sessionId = msg.getHeader().getSessionID();
String returnCode = "";
SurveyResponse td = new SurveyResponse();
try {
ClientRealData clientRealData = JsonUtils.toBean(body, ClientRealData.class);
//將客戶端的實時信息設置到在線客戶端的屬性中
Map<String, SurveyClient> onlineClientMap=(Map<String, SurveyClient>) AppContainer.onlineClientMap.get(clientRealData.getAppkey());
SurveyClient surveyClient=onlineClientMap.get(sessionId);
// logger.debug("【處理客戶心跳】客戶端【存在嗎】?:"+surveyClient!=null);
if(surveyClient!=null){
logger.debug("【處理客戶心跳】clientRealData:"+clientRealData.getClientCode()+"@"+clientRealData.getAppCode());
surveyClient.setClientCode(clientRealData.getClientCode());
surveyClient.setUpdateTime(new Date());
surveyClient.setWeight(clientRealData.getWeight() == null ? "60" : clientRealData.getWeight());
}
returnCode = "success";
} catch (Exception e) {
e.printStackTrace();
returnCode = "failure";
logger.error("【處理客戶心跳】消息失敗!異常:" + e.toString());
}
td.setCode(returnCode);
msg.setBody(td);
return msg;
}
根據業務的類型與企業客戶的級別,選擇一個可用的客戶端,這部分改變比較大,包括構建treemap得到權重與概率與級別要求,同類型還要按已經分配的任務決定給最少任務的。如果正好又不能用了,再遞歸找一個可用的。
public static SurveyClient getClientByUserRankAndClinetLever(String appCode,int userRank) {
// 從appCode得到appKey,從而找到可用的在線客戶端.讓前端根據appKey來分子任務不可靠
SurveyApp surveyApp = appCodeHolder.get(appCode);
logger.debug("【策略2】未配置客戶端,返回!appKey:"+appCode);
return getClientByUserRankAndClinetLeverByAppkey(surveyApp.getAppKey(),userRank);
}
private static SurveyClient getClientByUserRankAndClinetLeverByAppkey(String appKey,int userRank) {
// 從appCode得到appKey,從而找到可用的在線客戶端.讓前端根據appKey來分子任務不可靠
//SurveyApp surveyApp = appCodeHolder.get(appCode);
Map<String, SurveyClient> onlineSurveyClientMap = onlineClientMap.get(appKey);
if(onlineSurveyClientMap==null){
logger.info("【策略2】未配置客戶端,返回!appKey:"+appKey);
return null;
}
int onlineClientNum=onlineSurveyClientMap.size();
logger.debug("【策略2】surveyApp(code|在線客戶端數):"+appKey+"|" + onlineClientNum);
// 用於權重隨機的參數對象
//logger.debug("【策略】可選用客戶端的數:" + onlineClientNum);
Map<String, Integer> canUseClient = new HashMap<String, Integer>();//用於treemap排序。
List<SurveyClient> sameLeverClientList = new LinkedList<SurveyClient>();//用於同級別客戶端存放
if (onlineClientNum == 0) {
logger.info("【策略2】備選客戶端爲0,返回!");
return null;
}else {
Iterator iter = onlineSurveyClientMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, SurveyClient> entry = (Map.Entry<String, SurveyClient>) iter.next();
Integer eachWeight = new Integer(entry.getValue().getWeight() == null ? Constants.CLIENT_BASE_LEVER+"" : entry.getValue().getWeight());
canUseClient.put(entry.getKey(), eachWeight);
logger.debug("【策略2】循環可選用客戶端詳細信息(sessionId|weight):" + entry.getKey() + "|" + eachWeight);
if(onlineClientNum==1) return entry.getValue();
}
}
//找出可用的客戶端,產生一個map,再構建treemap,用策略得到一個值。
logger.info("【策略2】準備篩選的客戶端個數:"+canUseClient.size());//canUseClient不含有重複的,所以得到選擇的客戶端值,還要再處理多個同值的情況。
RankAndLeverTreeMapSelect rankAndLeverTreeMapSelect = new RankAndLeverTreeMapSelect(canUseClient,0);
Integer chooseValue = rankAndLeverTreeMapSelect.chooseValue();
logger.debug("【策略2】選擇出的客戶端lever:"+chooseValue);
if(chooseValue==null) return null;
for(SurveyClient surveyClient:onlineSurveyClientMap.values()){
if(surveyClient.getWeight() == null) surveyClient.setWeight(Constants.CLIENT_BASE_LEVER+"");
logger.debug("【策略2】當前比對的surveyClient.getWeight(null=60):"+(surveyClient.getWeight()));
//if(StringUtils.isBlank(surveyClient.getWeight())) continue;//沒有就是60分
if(chooseValue.intValue()==new Integer(surveyClient.getWeight()).intValue()) sameLeverClientList.add(surveyClient);
}
if(sameLeverClientList.size()==1) return sameLeverClientList.get(0);
//如果同一個值的客戶端有多個,再排序,取任務最少的一個。
Collections.sort(sameLeverClientList,new Comparator<SurveyClient>() {
@Override
public int compare(SurveyClient surveyClient1, SurveyClient surveyClient2) {
//以下如果改變順序則調換一下參數位置
return surveyClient1.getTaskCount()-(surveyClient2.getTaskCount());
}
});
SurveyClient surveyClient=sameLeverClientList.get(0);
sameLeverClientList.clear();
//如果找到的客戶端不能用,就遞歸找一個,同時移除這個客戶端
SecretManagement m = ServerGlobal.sessionWithAppKeys.get(surveyClient.getSessionId());
if(m!=null && m.getChannel()!=null && m.getChannel().isWritable()){
return surveyClient;
}
else{
Map<String, SurveyClient> surveyClientMap=AppContainer.onlineClientMap.get(appKey);
if(surveyClientMap.containsKey(surveyClient.getSessionId())){
logger.debug("【策略。推送失敗移除客戶端再遞歸獲取】sessionId:"+surveyClient.getSessionId());
surveyClientMap.remove(surveyClient.getSessionId());// 根據通訊客戶端,移除裏面的調查客戶端對象
}
return getClientByUserRankAndClinetLeverByAppkey(appKey,userRank);
}
}
/**rankAndLeverTreeMapSelect.chooseValue();
* 策略:
* 1.如果有用戶級別值,比如90分,那100、80、70、60、40、20分的客戶端中,選擇最近的80分的客戶端。
* 2.如果用戶沒有級別,那80/70/60/40/20中,選擇及格的最低的60,如果都不及格,選擇最高的40。
* 3.選擇了一個分值的客戶端,如果這裏面有多個,再隨機選擇一個(未來根據完成情況或者性能)
* <P></P>
* @return
*/
@Deprecated
public K choose() {
...
}
/**
* 考慮到重複情況,不能返回key了,只能返回特定value後再循環處理。
* @return
*/
public Integer chooseValue() {
if(treeMap.size()==0) return null;
if(treeMap.size()==1) return treeMap.firstEntry().getKey();
if(_userRank>0d){//如果有用戶級別
logger.debug("有用戶級別值,找接近最大的。_userRank:"+_userRank);
SortedMap<Integer, K> headMap = this.treeMap.headMap(_userRank, true);
logger.debug("_userRank & headMap.size:"+_userRank+"|"+headMap.size());
if(headMap.size()==0) return treeMap.firstEntry().getKey();//如果找不到,給最低的。
return headMap.lastKey();
}
else{//如果無用戶級別
logger.debug("無用戶級別值,找及格里最小的。_baseLever:"+_baseLever);
SortedMap<Integer, K> tailMap = this.treeMap.tailMap(_baseLever, true);
logger.debug("_baseLever & headMap.size:"+_userRank+"|"+tailMap.size());
if(tailMap.size()==0) return treeMap.lastEntry().getKey();//如果都生活及格,找一個最大的。
return tailMap.firstKey();
}
}
3.3 業務處理核心類的功能
3.3.1 TaskContainer主要的屬性
包括了背調任務存放,失敗處理隊列,子任務分發線程池,外部持久化接口實現,子任務完成監聽類。超時時間,嘗試次數配置。
/**
* 任務容器-管理背調任務與相關子任務
* @author liujun
* @date 2018年1月18日 上午10:33:01
*/
public class TaskContainer {
private static final Logger logger = Logger.getLogger(TaskContainer.class);
/** 實時總任務信息(taskid---TaskInfo(SubTaskInfoMap)) */
public volatile static Map<String, TaskInfo> taskInfoMap = new ConcurrentHashMap<String, TaskInfo>();
/** 任務在內存中允許的最大存放數 */
private static Integer maxTaskMapSize=Integer.MAX_VALUE;
/**
* 實時子任務信息(subtaskid---SubTaskInfo)
* 目的:子任務完成後,根據subTaskId從這裏快速拿到對應的子任務。從上面的主任務不方便找。
* 不需要了,子任務中帶有主任務ID,所以還是先拿主任務,再取子任務處理。
*/
// public volatile static Map<String, SubTaskInfo> subTaskInfoMap = new ConcurrentHashMap<String, SubTaskInfo>();
/**使用阻塞隊列,放置所有要處理的失敗子任務.失敗的任務先會再回線程池,之後超時會觸發返回*/
BlockingQueue<SubTaskInfo> subTaskInfoQueue = new LinkedBlockingQueue<SubTaskInfo>();
/**任務超時是否自動處理,此超時不是推送客端嘗試多次,而是等待子任務完成*/
public boolean autoDealTimeoutSubtask = false;
/**
* 子任務推送的最多嘗試次數
*/
public static int maxRePushSubTaskTimes=5;
/**
* 等待子任務完成的超時的時間
*/
// public static long maxTimeoutSubTaskDealTime=24*60*60000L;
/**線上子任務超時時間*/
public static long maxTimeoutOnlineSubTaskTime=60*1000L;
/**主任務超時時間*/
public static long maxTimeoutTaskTime=2*24*60*60*1000L;
/**
* 一個配置的client下的實時子任務信息(sessionId-List<SubTaskInfo>)
* 用sessionId方便應對底層的上下線變化。中間件的事件只能得到sessionId,沒有ClientCode。
*/
public volatile static Map<String, List<SubTaskInfo>> clientSubTaskInfoMap = new ConcurrentHashMap<String, List<SubTaskInfo>>();
/** app,client配置容器類 */
private AppContainer appContainer;
/** 子任務分派用線程池 */
private static ExecutorService executor = Executors.newCachedThreadPool();
/** 監聽器-子任務完成 */
public SubTaskListener subTaskListener = new SubTaskListener();
/** 監聽器-客戶端上下線事件 */
public ClientStatusListener clientStatusListener = new ClientStatusListener();
/** 任務池鎖 */
private Object TaskLock = new Object();
/** 任務處理線程 */
public SubTaskRedo subTaskRedo = new SubTaskRedo();
/**
* 外部持久化任務接口
*/
public TaskPersistenceInterface taskPersistenceInterface;
3.3.2 主要方法
接收背調任務及選擇客戶端後發出去
在initContainer中已經用TaskFactory,把請求參數處理成TaskInfo對象了,處理過程中已經持久化了。
String queryJsonStr = jsonParam.toString();
logger.info("【發起任務】背調任務請求參數:" + queryJsonStr);
if (containerInit != null) {
logger.info("---------【用戶發起背調了...】--------");
TaskInfo taskInfo = TaskFactory.creatTaskInfo(jsonParam);
taskContainer.createAndPutTaskPool(taskInfo);
} else {
logger.warn("【發起任務】背調中心沒有啓動");
}
taskContainer處理背調任務:
/**
* <P>
* 根據提交請求,生成主任務子任務,放入登記的map,並放入子任務隊列
* </P>
* @param paras
* @param appCode
* @throws SurveyException
*/
public boolean createAndPutTaskPool (TaskInfo taskInfo) throws SurveyException {
boolean createResult=false;
// TaskInfo taskInfo = TaskFactory.creatTaskInfo(paras);
logger.debug("【主任務Map添加】當前總數:" + taskInfoMap.size());
if (taskInfoMap.size() >= maxTaskMapSize) {
logger.error("【任務登記MAP】已滿!");
throw new SurveyException("任務登記已經滿");
// return false;
} else {
taskInfoMap.put(taskInfo.getTaskId(), taskInfo);
Iterator<Map.Entry<String,SubTaskInfo>> iter =taskInfo.getSubTaskMap().entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, SubTaskInfo> entry = (Map.Entry<String, SubTaskInfo>) iter.next();
try {
startExecutorCompletionService(entry.getValue());
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new SurveyException("任務提交線程池失敗",e);
}
}
logger.debug("【主任務池添加】當前總數+1後:" + taskInfoMap.size());
//改阻塞隊列不需要另加鎖
// synchronized (TaskLock) {
// TaskLock.notifyAll();
// }
createResult = true;
}
logger.debug("【任務登記MAP】情況如下:");
Iterator<Map.Entry<String,TaskInfo>> iter =taskInfoMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskInfo> entry = (Map.Entry<String, TaskInfo>) iter.next();
logger.debug("【任務登記MAP】主任務id|(總|推|執|完):"+entry.getValue().getTaskId()+"|"+entry.getValue().getTotalTask()+"|"+entry.getValue().getPushTask()+"|"+entry.getValue().getExecuteTask()+"|"+entry.getValue().getCompleteTask() );
}
return createResult;
}
上面的拆分後的子任務處理:startExecutorCompletionService(entry.getValue());
/**
* <P>將子任務設置一個可用的客戶端後,通過線程池執行發送</P>
*
* @param subTaskInfo
* @param taskContainer
* @throws InterruptedException
* @throws ExecutionException
*/
public void startExecutorCompletionService(SubTaskInfo subTaskInfo) throws InterruptedException, ExecutionException {
if (StringUtils.isBlank(subTaskInfo.getAppCode()) && StringUtils.isBlank(subTaskInfo.getAppKey())) {
logger.warn("【分派預處理】子任務未設置AppCode或者AppKey,子任務Id:" + subTaskInfo.getSubTaskId());
return;
}
if (!StringUtils.isBlank(subTaskInfo.getStatus())) {
//已經有狀態的,都是從庫里加載的,不處理了,只放池子裏。等線下的回調,或者人工處理,或者超時了。
logger.warn("【分派預處理】子任務已經有狀態,不再分配推 。子任務Id:" + subTaskInfo.getSubTaskId()+",狀態:"+subTaskInfo.getStatus());
// if(Constants.TASK_TYPE_ONLINE.equals(subTaskInfo.getSubTaskType()))
// subTaskInfoQueue.put(subTaskInfo);//如果成功的,並且是線上的,進行超時處理。
return;
}
// 從在線客戶端表中取一個
// SurveyClient surveyClient = AppContainer.getWeightRandomClient(subTaskInfo.getAppCode());
SurveyClient surveyClient =null;
if(StringUtils.isBlank(subTaskInfo.getAppKey())){
logger.debug("【老版本-按AppCode分配】-----------舊版getAppCode--"+subTaskInfo.getAppCode());
surveyClient=AppContainer.getClientByUserRankAndClinetLever(subTaskInfo.getAppCode(),0);
}else
{
logger.debug("【新版本-按Appkey分配】-----------新版getAppKey--"+subTaskInfo.getAppKey());
surveyClient=AppContainer.getClientByUserRankAndClinetLeverByAppkey(subTaskInfo.getAppKey(),0);
}
if (surveyClient == null || surveyClient.getClientCode() == null) {// 後面表示沒有業務心跳補充屬性
// logger.debug("");
logger.warn("【分派預處理】暫無可用的客戶端");
logger.debug("");
subTaskInfo.setExeErrorCount(subTaskInfo.getExeErrorCount()+1);
// subTaskInfoQueue.put(subTaskInfo);//沒客戶處理,則回爐
logger.debug("【分派預處理】原來回爐再次嘗試,現在直接返回任務推送失敗,再推無意義");
// 自動處理,作爲失敗子任務返回
JSONObject finishJason = new JSONObject();
finishJason.put("code", "failure");
finishJason.put("msg", "任務推送失敗");
JSONObject finishData = new JSONObject();
finishData.put("taskId", subTaskInfo.getTaskInfo().getTaskId());
finishData.put("subTaskId", subTaskInfo.getSubTaskId());
finishData.put("remark", "暫無可用的客戶端");
finishJason.put("data", finishData);
logger.debug("【子任務隊列消費】:【暫無可用的客戶端");
//通用處理完成或者失敗的子任務。推送試過了也就當子任務結束了。
updateSubTaskStatus(finishJason);
return;
}
//設置執行子任務的在線客戶端,之前有設置過其它的,也會被替換成當前的
String sessionId=surveyClient.getSessionId();
logger.debug("【分派預處理】策略選出的客戶端sessionId爲:" +sessionId );
subTaskInfo.setSurveyClient(surveyClient);
subTaskInfo.setClientCode(surveyClient.getClientCode());//標識最後一次使用的客戶端
//一個在線客戶端下所有的任務中加入此任務。用於客戶端下線後,更新下面的子任務
List<SubTaskInfo> subTaskInfoList =clientSubTaskInfoMap.get(sessionId);
if(subTaskInfoList==null) subTaskInfoList=new ArrayList<SubTaskInfo>();
subTaskInfoList.add(subTaskInfo);
// 交線程池執行分派子任務
Future<String> future = executor.submit(new AssignTask(subTaskInfo));
String exeResult = "";
try {
exeResult = future.get(15L, TimeUnit.SECONDS);
} catch (TimeoutException|ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.warn("【分派預處理】出錯e:" + e.getMessage());
exeResult = "failure";
} finally {
logger.info("【分派預處理】子任務分派結果爲:" + exeResult);
boolean isSubTaskSendOk = "success".equals(exeResult);
//推送後的維護
clientSubTaskInfoMap.get(sessionId).remove(subTaskInfo);//從當前客戶端下移除子任務
subTaskInfo.setSurveyClient(null);//子任務清除客戶端
if(!isSubTaskSendOk){
logger.info("【分派預處理】子任務分派不成功,重新回隊列subTaskId:" + subTaskInfo.getSubTaskId());
subTaskInfo.setExeErrorCount(subTaskInfo.getExeErrorCount()+1);
subTaskInfoQueue.put(subTaskInfo);//不成功,則回爐
}else{
subTaskInfo.setAsignStatus("success");//表示分配成功,等結果了
if(Constants.TASK_TYPE_ONLINE.equals(subTaskInfo.getSubTaskType()))
subTaskInfoQueue.put(subTaskInfo);//如果成功的,並且是線上的,進行超時處理。
}
//對主任務進行狀態標識
TaskInfo taskInfo = subTaskInfo.getTaskInfo();
synchronized (taskInfo) {
TaskInfo.modifyTaskInfoPush(taskInfo, isSubTaskSendOk);
}
}
}
Future future = executor.submit(new AssignTask(subTaskInfo));中的線程池任務。
/**
* <P>線程池執行發送任務</P>
*
* @author liujun
* @date 2018年1月15日 下午5:36:48
*/
static class AssignTask implements Callable<String> {
private SubTaskInfo subTaskInfo;
public AssignTask(SubTaskInfo subTaskInfo) {
this.subTaskInfo = subTaskInfo;
}
@Override
public String call() throws Exception {
// Thread.sleep(3000);
logger.info("【推送線程任務】任務執行...");
// if(true) return "success";//測試
// String body = subTaskInfo.getParaObj().toString();
//推送的子任務,只包括總任務ID,子任務ID,其它都是一個json中。
SubTaskData subTaskData=new SubTaskData();
subTaskData.setTaskId(subTaskInfo.getTaskInfo().getTaskId());
subTaskData.setSubTaskId(subTaskInfo.getSubTaskId());
subTaskData.setSubTaskType(subTaskInfo.getSubTaskType());
subTaskData.setQueryJsonStr(subTaskInfo.getParaObj().toString());
//這兩個用於異步任務時,客戶端按裏面的IP,PORT髮結果消息上來。
//不管是什麼,都加上這個。如果多箇中間層,那要按這個回覆信息。
subTaskData.setServerIp(ContainerInit.getInstance().appContainer.getServerIp());
subTaskData.setServerPort(ContainerInit.getInstance().appContainer.getServerPort());
String body =(JsonUtils.toString(subTaskData));
logger.debug("----------------【推送子任務】--------------------body:"+body);
// String appCode = subTaskInfo.getAppCode();
if (subTaskInfo.getSurveyClient() == null) {
logger.info("【推送線程任務】任務未設置執行客戶端:" + subTaskInfo.getDescription());
return "failure";
}
String clientSessionId=subTaskInfo.getSurveyClient().getSessionId();
logger.info("【推送任務任務】推送目標sissionId:" + clientSessionId);
boolean bln = ServerPushHandler.pushBySessionId(subTaskInfo.getSurveyClient().getSessionId(), "assignTaskToClient", body, new ServerPushTaskCallback());
logger.info("【推送任務任務】bln:"+bln);
// boolean bln =
// ServerPushHandler.pushByAppKey(subTaskInfo.getSurveyClient().getClientCode(),
// "assignTaskToClient", body);// 消息推送,推送所有服務器
//不管成功不成功,去除子任務與動態客戶端的關聯(成功就不要關聯了,不成功也應該換其它的客戶端了)
// List thisClientSubTaskList=clientSubTaskInfoMap.get(clientSessionId);
// if(thisClientSubTaskList==null) thisClientSubTaskList=new ArrayList<SubTaskInfo>();
// logger.info("【推送任務推送】當前session下的子任務數:" + thisClientSubTaskList.size());
// if (thisClientSubTaskList.size() > 0) {
// thisClientSubTaskList.remove(subTaskInfo);
// subTaskInfo.setSurveyClient(null);
// }
// Map<String, List<SubTaskInfo>>
//注意:【在返回值的furturn中處理移除或者再入隊的操作】
if (bln) {
logger.info("【推送任務推送】子任務成功");
subTaskInfo.setAsignStatus("success");
return "success";
} else {// 推送失敗
logger.error("【推送任務推送】子任務推送失敗:" + subTaskInfo.getSubTaskId());
subTaskInfo.setAsignStatus("failure");
//按說客戶端下線可以事件中移除,但偶爾出現沒有移除,所以在這裏將無法推送的,
//將這個不可用的channel的的客戶端移除
//注意:【在返回值的furturn中處理移除或者再入隊的操作】
Map<String, SurveyClient> surveyClientMap=AppContainer.onlineClientMap.get(subTaskInfo.getSurveyClient().getSurveyApp().getAppKey());
if(surveyClientMap.containsKey(subTaskInfo.getSurveyClient().getSessionId())){
logger.info("【推送失敗移除客戶端】sessionId:"+subTaskInfo.getSurveyClient().getSessionId());
surveyClientMap.remove(subTaskInfo.getSurveyClient().getSessionId());// 根據通訊客戶端,移除裏面的調查客戶端對象
}
return "failure";
}
}
}
接收客戶端任務完成的handler
將完成情況告訴配置進來的監聽器,監聽器會發起更新子任務的相關操作。
@Override
public MiddleMsg handleMsgEvent(MsgEvent dm, MiddleMsg msg) {
// TODO Auto-generated method stub
String body = msg.getBody() + "";
String code = "";
logger.debug("【得到Client子任務返回結果】API返回子任務結果:" + body);
SurveyResponse td = new SurveyResponse();
try {
JSONObject tdObject = JsonUtils.toJSONObject(body);
// 子任務失敗原因
subTaskListener.onSubTaskFinished(tdObject);
code = "success";
} catch (Exception e) {
e.printStackTrace();
code = "failure";
logger.error("【得到Client子任務返回結果】背調中心消息處理任務完成消息失敗!異常:" + e.toString());
}
td.setCode(code);
msg.setBody(td);
return msg;
}
監聽後發起的更新操作:
/**
* <P>
* 根據監聽的子任務完成結果,更新任務的狀態,都完成後有可能從總任務表中移除,並且調用外部接口,進行持久化操作。
* </P>
*
* @param finishJason
*/
public void updateSubTaskStatus(JSONObject finishJason) {
// 根據子任務完成情況,修改子任務的狀態,以及主任務的狀態
logger.info("【修改子任務的完成狀態】:"+finishJason.toString());
try {
String code = finishJason.getString("code");
String msg = finishJason.getString("msg");
String subTaskId = finishJason.getJSONObject("data").getString("subTaskId");
String taskId = finishJason.getJSONObject("data").getString("taskId");
String remark = (finishJason.getJSONObject("data").containsKey("remark"))? finishJason.getJSONObject("data").getString("remark"):"";
// String clientCode = finishJason.getString("clientCode");
TaskInfo taskInfo=taskInfoMap.get(taskId);
if(taskInfo==null){
logger.info("【修改子任務的完成狀態】內存中已經沒有這個主任務了!taskId:"+taskId);
//如果需要,找不到主任務了,還可以直接入庫
//如果非線上任務,有可能內存中沒有了,因爲非線上,內存中存在的時間太長了,佔用比較大
//是否都完成,以及上報都在接口中實現
if(taskPersistenceInterface!=null) taskPersistenceInterface.modifySubTask2DB(finishJason);//子任務入庫
return;
}
SubTaskInfo subTaskInfo = taskInfo.getSubTaskMap().get(subTaskId);
// if(Constants.TASK_TYPE_ONLINE.equals(subTaskInfo.getSubTaskType())){
// logger.info("【修改子任務的完成狀態】線上子任務不能人工處理!subTaskInfo.getSubTaskType():"+subTaskInfo.getSubTaskType());
// //如果需要,找不到主任務了,還可以直接入庫
// return;
// }
//1.【收到】
if("received".equals(code)) {
logger.info("【修改子任務的完成狀態,並移除】子任務爲異步的,對方已經收到!code:"+code);
subTaskInfo.setStatus(Constants.TASK_DOING);
subTaskInfo.setDescription("子應用已經收到子任務");
synchronized (taskInfo) {
taskInfo.setReceivedAsyncTask(taskInfo.getReceivedAsyncTask()+1);
}
//這裏啥也不做。因爲是異步的,只是對方已經收到了。
return;
}
if(Constants.TASK_FAILURE.equals(subTaskInfo.getStatus()) || Constants.TASK_DONE.equals(subTaskInfo.getStatus()) ){
//這裏啥也不做。可能總任務檢測時設置了結果
return;
}
subTaskInfo.setSurveyClient(null);
// subTaskInfo.setUpdateTime(new Date());//數據持久化時再設置
//2.【不成功】 默認成功。成功時...(一定是執行且成功的)
if(!"success".equals(code)) {
logger.info("【修改子任務的完成狀態,並移除】子任務推送或者執行出錯了!返回code:"+code);
subTaskInfo.setStatus(Constants.TASK_FAILURE);
subTaskInfo.setDescription(msg+remark);//中文失敗與原因
//這裏是否加入出錯隊列再處理?還是先入庫,以後從庫中加載呢?都可以,目前先入。如果推送失敗的,已經推過多次了,如果執行失敗的,先不再推送了。
// return;
}//3.【成功】
else{
logger.info("【修改子任務的完成狀態,並移除】子任務執行成功!code:"+code);
String hasData = (finishJason.getJSONObject("data").containsKey("hasData"))? finishJason.getJSONObject("data").getString("hasData"):"";
Integer dataCount =0;
try {
dataCount = (finishJason.getJSONObject("data").containsKey("dataCount"))? finishJason.getJSONObject("data").getInt("dataCount"):0;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
subTaskInfo.setHasData(hasData);
subTaskInfo.setDataCount(dataCount);
subTaskInfo.setStatus(Constants.TASK_DONE);
}
if(taskPersistenceInterface!=null) taskPersistenceInterface.modifySubTask2DB(subTaskInfo);//子任務入庫
synchronized (taskInfo) {//計算完成數
//執行數據+1(包括推失敗的,對方收到的異步的),完成數要看成功才成。
TaskInfo.modifyTaskInfoFinish(taskInfo, "success".equals(code));
// logger.info("【修改子任務的完成狀態,並移除】總任務【移除前】有:"+taskInfoMap.size());
// logger.info("【修改主任務的狀態,並可能移除】當前主任務的-總|執|完|線:" + taskInfo.getTotalTask() + "|" + taskInfo.getExecuteTask()+ "|" + taskInfo.getCompleteTask()+ "|" + taskInfo.getOnlineTask());
}
//del--->當執行數與線上數一樣時。線上都完成了。就持久化,但不移除。當與總數一樣時,持久化並移除。
//都執行了就移除,並持久化。但如果不全是線上的,置的狀態不一樣
if (taskInfo.getExecuteTask().intValue() == taskInfo.getTotalTask().intValue()) {
//【移除的情況:】當線上數與總數一樣的時候,全完成了。就從內存中移除,並且調外部接口類進行持久化。線下子任務沒有超時機制,可能一直接沒反饋,由主任務總超時處理。
// if (taskInfo.getOnlineTask().intValue() == taskInfo.getTotalTask().intValue() || taskInfo.getExecuteTask().intValue() == taskInfo.getTotalTask().intValue() ) {
//如果沒有異步的任務
if (taskInfo.getReceivedAsyncTask()==0 ) {
//主任務狀態爲:
boolean isAllSubtaskOk=taskInfo.getCompleteTask().intValue()==taskInfo.getExecuteTask();
taskInfo.setStatus(isAllSubtaskOk?Constants.TASK_DONE:Constants.TASK_FAILURE);
// if(taskInfo.getCompleteTask().intValue() == taskInfo.getExecuteTask().intValue()) taskInfo.setStatus(Constants.TASK_FAILURE);
// taskInfoMap.remove(taskInfo.getTaskId());// 從任務記錄中移除
// if(taskPersistenceInterface!=null) taskPersistenceInterface.modifyTask2DB(taskInfo);//主任務入庫
logger.info("【修子任務的完成狀態,全部完成並移除】移除任務id:" + taskInfo.getTaskId());
}else{
//【持久化】都移除,線下不適合在內存中長時間放。
taskInfo.setStatus(Constants.TASK_DOING);
logger.info("【修子任務的完成狀態,只持久化線上部分】任務id:" + taskInfo.getTaskId()+"。此時收到線下任務回覆數爲:"+taskInfo.getReceivedAsyncTask());
}
taskInfoMap.remove(taskInfo.getTaskId());// 從任務記錄中移除(線下的不適合長時間放在內存中)
logger.info("【修改任務的完成狀態,全部完成並移除】有完成後移除。總任務【移除後】有:" + taskInfoMap.size());
if(taskPersistenceInterface!=null) taskPersistenceInterface.modifyTask2DB(taskInfo);//主任務入庫
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.info("【修改子任務的狀態,並移除】出錯:"+e.toString());
}
}
其它方法介紹
監聽客戶端下線時(客戶端管理裏也有去監聽),將其它持有的子任務中標識的客戶端置空,並移除重新選擇客戶端。
public synchronized void updateSubTaskInfoByOffline(String clientSessionId)
定時任務:清理、失敗任務沒超時,沒超重試次數時的再次執行。
/**
* <P>守護線程中定時會清理超時的主任務。(線上子任務等待結果超時在隊列裏處理,線下超時不處理,由主任務超時處理)</P>
*/
public void dealTimeoutTask(TaskInfo taskInfo)
/**
* <P>用於處理阻塞對列裏的子任務,再扔進線程池。</P>
* 1.反覆處理再分配的子任務,一定次數後超時。主要是原通訊沒返回sessionId,監聽移除有問題,所以多試幾次。
* 每次試如果不能會真正移除不在線的客戶端。
* 2.處理線上子任務,已經推送出去了,狀態發生變化。反覆檢測是不是超時沒有收到反饋。有反饋的會設置子任務狀態,就不再入隊了。
* @author liujun
* @date 2018年1月22日 上午9:59:10
*/
private class SubTaskRedo implements Runnable
此外,還有從持久層加載可以再執行的任務,提供人工控制的接口方法等。
4. 中間層的設計
4.1 作爲客戶端的核心處理類AsClientContainer
這個對接前面提到的SS。
public class AsClientContainer {
private static final Logger logger = Logger.getLogger(AsClientContainer.class);
public static final long THEARTBEAT_INTERVAL = 3 * 1000L;
private static JSONArray REMORTSERVERS=new JSONArray();
public static Map<String,JSONObject> REMORTSERVERS_MAP =new HashMap<String,JSONObject>();
/*子任務存放*/
// public volatile static Map<String, JSONObject> subTaskInfoMap = new ConcurrentHashMap<String, JSONObject>();
/**使用阻塞隊列,放置所有要處理的子任務*/
public volatile static BlockingQueue<JSONObject> subTaskInfoQueue = new LinkedBlockingQueue<JSONObject>();
/**
* 需要人工干預的子任務池
*/
public volatile static Map<String, JSONObject> subTaskInfoMap = new ConcurrentHashMap<String,JSONObject>();
/*同步對象存放*/
public volatile static Map<String, PushFuture<CommonReturnData>> syncKey = new ConcurrentHashMap<String, PushFuture<CommonReturnData>>();
/**用戶單例線程鎖*/
private static Boolean lockSigleton = true;
/**用戶單例對象*/
private static AsClientContainer clientContainer;
public TaskConsumer taskConsumer = new TaskConsumer();
public TimeoutSubTask timeoutSubTask = new TimeoutSubTask();
public OfflineSendInterface offlineSendInterface;
public DateFormatInterface dateFormatInterface;
/** 失敗的任務用線程池 */
private static ExecutorService executor = Executors.newCachedThreadPool();
private ScheduledExecutorService executorTimeout = Executors.newScheduledThreadPool(1);
public static AtomicLong subTaskNum = new AtomicLong();
任務消費,對重試任務,線上任務,人工任務都分別進行處理,人工任務的處理由外部提供處理類,真正實現由dubbo完成。
private class TaskConsumer implements Runnable {
@Override
public void run() {
while (true) {
try {
//因爲這個隊列中都是出問題的子任務,所以要等待一下處理。
Thread.sleep(1500);// 調節頻率,過快容易撐死~~
// logger.debug("【子任務隊列】的任務數1:" + taskNum);
JSONObject subtaskInfo=subTaskInfoQueue.take();
logger.debug("【子任務隊列消費】取出子任務JASON:"+subtaskInfo.toString());
String subTaskType=subtaskInfo.containsKey("subTaskType")?subtaskInfo.getString("subTaskType"):null;
//!!!!!!檢查這個子任務是還否可以複製之前的結果,如果可以就複製出來,返回一個成功的結果。
//這裏交馮實現的接口,由於工作變動,還沒出來。
// logger.debug("【子任務隊列】的任務數2:" + subTaskInfoQueue.size());
//如果重試了50次或者超時了5分鐘,那麼子任務失敗吧
long reDoTime=new Date().getTime()-subtaskInfo.getLong("startDate");
logger.debug("reDoTime:"+reDoTime+"。testnum:"+subtaskInfo.getInt("testNum"));
if(subtaskInfo.getInt("testNum")>Integer.parseInt(MiddleConfig.getFailureSubTaskMaxRetryTimes()) || (reDoTime>Long.parseLong(MiddleConfig.getFailureSubTaskMaxRetryTimes())) ){
logger.debug("【子任務隊列消費】子任務超時失敗:"+subtaskInfo.getString("subTaskId"));
logger.debug("【子任務隊列消費】子任務超時失敗,嘗試次數爲:"+subtaskInfo.getInt("testNum"));
AsClientContainer.subTaskInfoMap.remove(subtaskInfo.getString("subTaskId"));
//通用處理完成或者失敗的子任務
Thread.sleep(1000);// 調節頻率,過快容易撐死~~
String isAsync=subtaskInfo.containsKey("isAsync")? subtaskInfo.getString("isAsync"):null;
//1.【推失敗了,如果是異步的,就發消息給服務端】
if("async".equals(isAsync) || !Constants.TASK_TYPE_ONLINE.equals(subTaskType)){
// AsClientContainer.sendTaskAsyncResult2Server(asyncServerCode,finishJason);
//如果異步調用失敗。
logger.debug("【推送異步任務】超時了,發消息給服務器");
JSONObject finishJason=new JSONObject();
finishJason.put("code", "failure");
finishJason.put("msg", "OFFLINE_RPC_FAIL中間層任務調用C端失敗");
finishJason.put("data", subtaskInfo);//這裏面有ip/port用於異步。
//如果是線下的推送或者調用失敗了,只持久化到本地,再重試。或者超時。不可以迅速返回失敗的。
sendTaskAsyncResult2Server(finishJason);
return;
}
//2.【如果是線上任務推送失敗,設置同步等待對象。】
JSONObject finishJason=new JSONObject();
finishJason.put("code", "failure");
finishJason.put("msg", "中間層任務推送失敗");
JSONObject finishData=new JSONObject();
finishData.put("taskId", subtaskInfo.getString("taskId"));
finishData.put("subTaskId", subtaskInfo.getString("subTaskId"));
finishData.put("remark", "重試了"+subtaskInfo.getInt("testNum")+"次,用時"+reDoTime+"ms");
finishJason.put("data", finishData);
PushFuture<CommonReturnData> responseFuture = AsClientContainer.syncKey.get(subtaskInfo.getString("subTaskId"));
if(responseFuture!=null){
CommonReturnData response=new CommonReturnData();
response.setCode("failure");
response.setMsg("任務超時失敗");
response.setData(finishData);
responseFuture.setResponse(response);
logger.debug("【推送任務任務】超時了,設置同步對象的返回值");
}
else{
logger.debug("【推送任務任務】設置超時時,同步對象已經被移除。");
}
}
else//如果是正常處理子任務
{
//如果非線上任務,就走外部接口(注入的實現類)發出去(實現類會持久化,再發的)
logger.debug("subTaskType:"+subTaskType+"。offlineSendInterface:"+offlineSendInterface);
if(!Constants.TASK_TYPE_ONLINE.equals(subTaskType)){
try {
if(offlineSendInterface!=null){
offlineSendInterface.sendOfflineQuery(subtaskInfo);
}else{
logger.warn("【找不到外部(非線上子任務)調用的接口】");
throw new SurveyException("找不到外部(非線上子任務)調用的接口實現類");
}
logger.info("【推送任務任務】成功推送非線上任務到C端!");
} catch (SurveyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.warn("【推送任務任務】推送非線上任務到C端失敗!");
ReDoTask reDoTask = new ReDoTask(subtaskInfo);
executor.submit(reDoTask);
}
}
else //如果是線上的,就推送出去。
{
SurveyClient surveyClient = AsServerContainer.getClientByUserRankAndClinetLever(0);
// 如果找到策略的客戶端
if (surveyClient != null && surveyClient.getSessionId() != null) {
// 開始推送
String body = JsonUtils.toString(subtaskInfo);
String clientSessionId = surveyClient.getSessionId();
logger.info("【推送任務任務】推送目標sissionId:" + clientSessionId);
boolean bln = ServerPushHandler.pushBySessionId(clientSessionId, "assignTaskToClient", body, new MiddlePushClientTaskCallback());
logger.info("【推送任務任務】bln:" + bln);
if (bln) {
logger.info("【推送任務任務】成功!");
// 推成功了,但一直不返回,也是個問題。不過同步對象會被移除的。
} else {
logger.warn("【推送任務任務】推送失敗!");
ReDoTask reDoTask = new ReDoTask(subtaskInfo);
executor.submit(reDoTask);
}
} else {// 沒有可用的客戶端
logger.info("【推送任務任務】bln:沒有可用的客戶端,直接失敗返回。次數:" + subtaskInfo.getInt("testNum"));
ReDoTask reDoTask = new ReDoTask(subtaskInfo);
executor.submit(reDoTask);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4.2 作爲服務的核心處理類AsServerContainer
public class AsServerContainer {
private static final Logger logger = Logger.getLogger(AsServerContainer.class);
/** 實時總任務信息(taskid---TaskInfo(SubTaskInfoMap)) */
public static volatile Map<String, SurveyClient> onlineClientMap=new ConcurrentHashMap<String,SurveyClient>();
public boolean isMiddlewearStarted=false;
/** 任務在內存中允許的最大存放數 */
// private static Integer maxTaskMapSize=Integer.MAX_VALUE;
/**
* 可接入的類型列表,目前一箇中間層只支持一種類型的接口接入。(同一類型的app都一樣,不同的不一樣)
*/
private static List<ClientApp> clientAppList=new ArrayList<ClientApp>();
// static{
// //設置有效的客戶端
//
// }
/**
* 失敗子任務重複處理次數、與超時處理的時間
*/
public static int maxReDealSubTaskTimes=20;
/**
* 失敗子任務重複處理次數、與超時處理的時間
*/
public static long maxTimeoutSubTaskTime=60000L;
/**主任務超時時間*/
public static long maxTimeoutTaskTime=150000L;
/**
* 一個配置的client下的實時子任務信息(sessionId-List<SubTaskInfo>)
* 用sessionId方便應對底層的上下線變化。中間件的事件只能得到sessionId,沒有ClientCode。
*/
// public volatile static Map<String, List<SubTaskInfo>> clientSubTaskInfoMap = new ConcurrentHashMap<String, List<SubTaskInfo>>();
/**用戶單例線程鎖*/
private static Boolean lockSigleton = true;
private static AsServerContainer asServerContainer;
5. 客戶端的設計
5.1 核心類的設計
屬性如下,主要功能有子任務執行與客戶端業務心跳。
/**
* 背調中間層容器-管理通訊層並處理背調任務
* @author liujun
*/
public class ClientContainer {
private static final Logger logger = Logger.getLogger(ClientContainer.class);
public static final long THEARTBEAT_INTERVAL = 3 * 1000L;
/**appKey-同類的客戶端相同*/
private String appkey;
/**appSec-同類的客戶端相同*/
private String appSecret;
/**客戶端標識Code*/
private String clientCode;
/**所連接服務器IP*/
private String serverIP;
/**此客戶端的權重*/
private String weight;
/**通訊層客戶端*/
Client client = null;
/**子任務處理接口對象*/
ApiInvorkerInterface apiInvorkerInterface;
/**監聽器-監聽通訊層客戶端狀態*/
ClientStatusListener clientStatusListener;
/**客戶端上報狀態傳輸對象*/
ClientRealData clientRealData;
/**客戶端是否連接狀態標識*/
private boolean isConnected = false;
private Object lock=new Object();
// ScheduledExecutorService service = Executors.newScheduledThreadPool(1);//不需要線程池,只要一個循環的守護線程
/**用戶單例線程鎖*/
private static Boolean lockSigleton = true;
/**用戶單例對象*/
private static ClientContainer clientContainer;
5.2 背調任務執行
每一個包裝的Web客戶端,要實現這個接口來做具體任務。
/**
* 調用第三方功能的接口,請各個第三方接口應用實現些接口
* <P></P>
* @author liujun
* @date 2018年1月12日 下午5:10:02
*/
public interface ApiInvorkerInterface {
/**
* 根據請求參數與所選擇的一組app產生任務並處理
* <P></P>
* @param paras
* @param appCode
*/
public CommonReturnData dealSubTaskByApi(JSONObject taskData) throws SurveyException;
}
6. 其它
6.1 與Web應用的整合
外部Web應用提供數據持久化與具體任務執行等接口的實現。實現都是spring容器中的類,由一個統一管理的@Componet類@Autowired這些實現,在其afterPropetiesSet方法時啓動核心業務組件,並按提供的接口注入實現類。當然也可以考慮都交給spring管理。
6.2 子任務相關Web工程
近20個客戶端我做了一個例子工程,後來交辦出去發現在clone工程,於是我要求只用一個工程,各種實現類通過配置的不同進行加載。
6.3 反思
除了前面提到的通訊層更加獨立外,細節問題不少,比如線程池使用不夠規範。
有些參數不完全確定就用jason。一些參數要根據測試從外部配置進來。
內部系統的啓動停止最好由smartCycle控制,這裏用afterPropetiesSet只管啓動,沒有優雅的停止。
有些優化還需要相關應用與人員配合才能整體優化,比如通訊層,配置服務。
如果要高可用,還有非常多的工作要做。
雖然很多問題,但運行穩定,可以順利的工作。由於手上還有其它任務,比如企業服務,調查發起,定單處理,被查人同意,根據結果費用覈算等應用與功能處理。所以除了功能變更,沒真正進行重構過。