搭建推送中心(一)

在已通過去的2016年,整個後端技術市場上,若是說哪一種技術最火,我想微服務應該無人能夠出其右吧,能夠說火爆大江南北。不少公司的後端服務架構體系都在逐漸從單應用體系轉型到微服務體系,咱們也不例外的投入這股轉型浪潮中。能夠說,在可預見的將來幾年,微服務的架構體系定將在整個系統架構中成長爲蒼天大樹。

A、簡單提提簡單的一些技術選型評估

在作微服務中,須要作不少技術的評估,選型等等,好在這些使人頭疼的事不須要個人參與(小菜鳥一枚,上不得檯面),那麼在談到如今的微服務架構,每每都會把其中的調度協議抓出來大談特談一番,被暢聊的比較多的多是:基於高性能的遠程調用rpc協議以及很是輕量級的基於請求-響應模式的http協議。因此可能被提的比較多的幾個關鍵字大概是:dubbo,thrift,restful,以及後邊咱們最後選定的spring全家桶 spring cloud。在這裏不想說去作框架之間的偏激的比較,任何一種技術框架都有其側重點,以及相對的弱化區,在小子看來,技術框架歷來都沒有強弱的概念,只要能夠解決自身業務痛點的技術,都是好技術。java

接下來,說一下爲何咱們最後會選擇使用目前來講沒有很火的spring cloud吧。之因此選擇它,第一天然是基於對於spring社區幾乎絕對的信任。對於整個Java開發體系來說,spring社區的影響力之大,便如好萊塢與電影行業之發展同樣,因此做爲一個javaer,自己對於spring出品的東西,都抱着很是高的期待與信任。第二點是考慮到更爲輕量的restful協議,至於說有沒有比遠程調用協議更優越,小子也不知道(沒有使用過rpc協議作過東西,因此沒有更多的去了解)。第三點考慮則是基於spring boot微應用理念開發,自己spring cloud就是由spring社區整合旗下的其餘框架造成的一個全新的生態,而spring boot微應用的理念簡直就是爲了微服務架構而生(略偏激)。很是期待,徹底相信spring cloud在將來必定能夠大放異彩……spring

那在作評估與選型的時候,是否有其餘更多其餘的考慮,我暫時也不知道,若是有的話,我後續會再補充進來。這邊推薦一個連接,基於DD老師分享的關於dubbo與spring cloud的一些簡單對比:http://blog.didispace.com/microservice-framework/後端

 

B、簡體提提搭建系統消息推送模塊

推送模塊是我接手微服務架構的第一個應用的開發,而且整個系統的設計過程都是由我本身來驅動,自我思考,而老大則是做爲建議參考,從旁指導。(菜鳥一枚,羞澀羞澀)因此我才考慮將本身的一些感悟以及思考記錄下來,若是有哪一位網友不當心看到,而且對於他提供了一些微不足道的幫助,我想我花的時間與精力來寫這一系列文章(後續在持續改進推送模塊中,若是有好的改動,好的設計,或者優化,我會持續寫,能夠當成一個簡單的系列)的目的就達到啦。由於開源,分享,可使我快樂。微信

1、基礎平臺搭建

公司如今處於發展階段,目前對接了幾種推送的第三方平臺:短信平臺,app推送平臺,以及微信公衆號推送。至關的簡陋,由於如今初步轉型,後續業務上確定會持續對接各大平臺來作消息推送,因此初步的設計須要作好可擴展的考慮。(不過請注意,切勿過分設計……)restful

首先,基於咱們選擇了spring cloud來作平臺架構,協議上的選擇無疑就是很是輕量,而且極爲成熟的基於http的restful協議。因此暫時性的,整個推送中心咱們的對於提供兩種接口,一種是https的rest請求,一種則是基於消息推送的消息隊列。數據結構

底層第三方平臺的對接,咱們採用的是是spring 提出的starter的形式,經過pom文件引入,這種形式對於後續的第三方平臺持續追加都是毫無壓力的,很是輕鬆,而且很是輕量;因此第一步的框架構建大概以下圖架構

(圖1)app

在starter上提供一個service來處理不一樣的消息,以及即將調用的不一樣的第三方平臺。starter雖然看似簡單,實則不容易,特別是咱們這種多系統,多平臺的對接狀況(接入參數的多樣性)。框架

2、水平擴展的消息服務層搭建

推送模塊就是來作消息推送的,因此離不開業務的格局,因此有必要分析具體的業務。基於業務的設計基本上每一個公司都有不一樣的業務需求,因此結果天然也是天差地別,這裏以咱們公司的業務做爲說明記錄,消息推送主要是以系統內部觸發消息,以消息隊列的形式來觸發推送,不少時候都是一些提醒,好比短信提供,公衆號提醒;另外就是經過調用推送接口來給指定用戶推送內容服務,活動等等;第三點經過接口調用做爲其餘平臺整合,擴展使用。因此我初步把咱們的消息類型分類,提取幾個拿出來講,以下圖:函數

(圖-2)

一、入口設計

仔細考慮,好比一個註冊消息觸發,須要作短信推送,把註冊相關的內容(送代金券之列)推送到用戶手中。好比一個收款的消息觸發,須要短信提醒交易狀況,須要公衆號提醒交易額,須要app推送相關流水等等……一條消息進來,有時候僅僅須要單個平臺進行推送,有時候卻須要多個平臺同時去作推送。因此消息處理的一套集合大概是  <消息類型 * 平臺>這樣一個結果集,如此凌亂,可能後續很是臃腫的結果集,若是請求的入口設計(其餘服務調用推送服務必須遵循的協議)不合理,後續根本沒法持續維護。

初步設計:提供一個公共的數據模型DTO(數據傳輸對象 data transfer object),以及一些推送系統內部固定的常量的jar包,目的是爲了讓我後續的業務不斷擴展能夠帶來極大的方便性,因此個人DTO必須在最開始就儘量考慮周全(固然,彷佛也沒有多少好考慮的,哈哈)

public class SenderMessageDTO {
	
	/** 消息平臺類型*/
	private String sender;
	
	/** 平臺標識*/
	private String 	sys;
	
	/** 消息類型*/
	private String 	msgType;

        // .......
}

有了這個jar包以後,整個推送的入口算是敲定了,其餘服務如果須要使用推送服務,那麼只須要引入jar包,而後按照我實現約定好的協議來配置它們須要的內容,包括推送方式,推送時間,推送通道等等。

入口敲定以後,我既定的下一層屬於消息服務層;

二、簡單的消息服務層設計

如圖2所畫,整個服務層的架構是水平方向擴展的,因此可能咱們想到的第一點就是實現接口,來達到統一入口的目的。實現方式以下:

/**
 * 消息處理器
 * @author lennon
 * 2017年3月8日 下午2:45:20
 */
@Service
public class MessageService {
	
	/**
	 * 發送消息:調用消息的處理器
	 * @param senderMessageVO
	 */
	public String send(SenderMessageVO senderMessageVO){
		IMessageServiceType iMessageServiceType = MessageServiceTypeHolder.getMessageServiceType(senderMessageVO.getMsgType());
		if(iMessageServiceType != null) {
			return iMessageServiceType.messageHandler(senderMessageVO);
		}
	}
}

首先定義一個消息服務類,考慮消息種類繁多:註冊,登陸,充值,下單……因此,在作水平擴展的時候,有兩個方案:

        1、以繼承體系來維護,即定義好消息基礎類(抽象類,或者普通類),目的就是,水平擴展的子類只須要繼承這個基礎類一次(Java不容許多繼承的方式),因此從目前分析的角度來講,這種方式並無太大問題。

        2、基礎類不變,抽取公共接口,讓全部消息業務類實現接口。這麼作的方式就不存在說多繼承的問題,而且水平擴展也毫無障礙。

從上邊的代碼均可以看出最後選擇的是第二種方案,具體實現是:定義消息類型(以枚舉類型,或者字符串常量),消息業務類實現了一個接口

/**
 * 消息類型 處理接口
 * @author lennon
 * 2017年3月8日 下午2:46:07
 */
public interface IMessageServiceType {

	public String messageHandler(SenderMessageVO messagevo);
	
}

按道理將,如今的消息體系的調用狀況應該以下:

http/https 請求進入推送服務中,映射到action層進行處理,action層注入了messageservice對象進行業務處理。messageservice注入holder對象。那麼這個holder是用來作什麼的?很明顯,holder實際上是一個HashMap,經過註解與接口實現來掃描到全部消息類型業務處理的單例對象,而後保存在HashMap中。目的就是:經過messageservice就能夠以SenderMessageDTO的對象中msgType精確的找到消息類型業務處理對象。引入這種holder的模式,是基於系統後續擴展(百分之百確認)來考慮,只須要實現接口,加入註解,不須要改變messageservice的代碼,也不會影響其餘的代碼(基於開閉原則)。

3、水平擴展的消息發送器(基於starter之上的一層調用封裝)

在設計完消息服務層以後,須要考慮的是如何發送消息,應該說,如何簡單的發送消息,而且從代碼設計層面考慮如何毫無壓力引入其餘平臺的設計。在starter引入以後,簡單的調用starter來發送消息,實際上是很是簡單,而且毫無壓力的,那麼爲什麼還須要作這樣的一層設計?舉個例子,假如如今有十個公衆號,如何調用微信接口來推送消息呢?簡單。調用分裝好的starter接口,選擇好哪一個公衆號的信息就OK。

閉眼思考,過度簡單的東西容易讓人懷疑,因此嘗試死磕本身吧(羅振宇最常提的一句話,死磕本身)。若是不作分裝,這一層的代碼放在消息服務層,是否是有十個公衆號,就要有十層的if - else來斷定公衆號?有人說,statert上分裝了微信調用的接口,能夠在這一層做考慮,把公衆號到 信息丟到starter去,而後直接調用。絕對不能夠,斬釘截鐵的肯定的告訴你,不能夠!爲什麼?

請思考starter的優點!

starter不是推送服務專屬服務,它有不少其餘的事要作,因此它只能維護其餘平臺的接口對接,至於上一層如何調用,堅定不能嵌入starter中,一個臃腫的starter註定不是好的starter。

不談其餘可能有點糟糕的設計,聊聊本身的怎麼寫(站在如今的小子的經驗以及認知的角度,小子認爲不能更好了的設計),設計與消息服務的設計類似:

/**
 * 消息發送接口
 * @ClassName: Sender 
 * @author lennon
 * @date 20170307
 * @since JDK1.8  
 */
@Service
public class Sender {
	
	/**
	 * 發送消息
	 * @param messageInfo
	 */
	public MessageWithObjectVO sendMessage(SenderMessageVO messageInfo){
		ISenderType iSenderType = SenderTypeHolder.getSender(String.valueOf(messageInfo.getSender()));
		if(iSenderType != null) {
			return iSenderType.sendMessage(messageInfo);
		}
		return CommonMessageEnum.OPERATE_ERROR.getMessage();
	}
}

從消息發送器的設計角度來分析,sender經過holder來調用不一樣的第三方平臺對接的上層接口(第三方平臺底層是starter封裝)由於不想要直接調用starter接口,因此纔會有了sender的這個設計與實現。後續添加各類starter的時候,messageservice容易形成各類改動,這是不容許的。之後再添加starter第三方接口,實現senderType而後來調用新的平臺,新平臺的引入對於消息服務不會產生直接影響。

這裏舉個例子來分析微信公衆號的推送(中間經歷了一次改動):

   a、按照公衆號來分類形式,經過註解+實現接口的形式能夠將十個公衆號分爲十個類,而且後續假如增長其餘公衆號推送,依舊是註解+實現接口,根本不須要作很大的代碼改動。(看起來的感受是完美的?)

    b、經老大點醒,這裏是屬於嚴重的過分設計,仔細分析公衆號之間的調用區別,其實僅僅只是key與srcret的不一樣而已,若是爲此去搞很是多類,那之後若是有一百個公衆號,是否是要維護一百個類?想一想就打了個機靈。他提供的解決方案是:定義好一個數據結構,來存放全部的key與secret,說到底無非就是

Map<String, Map<String, String>> map;

即使是一百公衆號,對於系統而言,不外乎就是往配置文件增長一百個新的key與secret。

4、消息服務層  調用 消息發送器

說完消息服務,說完消息發送器,剩下的就是消息服務來對接消息發送器了。

這裏存在比較大的問題是:不一樣的消息可能須要根據不一樣的需求來調用不一樣的消息發送器。

有兩種方案:經過爲每一個具體的消息類型服務定義一個接口來分別實現第三方平臺接口,由於不一樣的業務須要作不一樣的業務處理。另外就是不要搞得那麼複雜不一樣的平臺接口就寫不一樣的函數,按照須要調用,類稍微龐大一點點而已。

……並無什麼好的方案,悲劇。

整個的大概的設計過程相似醬着……

 

歡迎指教!

相關文章
相關標籤/搜索