老實說,多線程在web開發裏面很是常見,不少web容器自己就支持多線程,因此不少時候咱們在進行web開發的時候並不須要考慮多線程相關的負責問題,而只須要實現相關的業務功能便可。因此,能夠歸納地講,不少時候的web開發,並無多線程方面的考慮,由於web應用自己就是在多線程基礎上的了。前端
可是,有些時候爲了提升程序性能,在用戶的一個請求中中若是包含過多的業務操做或者包含耗時比較長的業務操做,咱們就須要考慮使用異步的方式來提升程序響應的速度了。這篇博客簡單介紹了在java中如何使用多線程實現一個簡單的異步框架。 java
這個事件異步處理框架主要的工做過程是這樣的:經過producer類對事件實體類序列化後,存儲在redis的list隊列中,而comsumer則負責讀取事件隊列中的事件模型對象並反序列化後調用相應的handler實現類對象進行事件處理,這些handler實現類的對象,經過spring完成handler具體類的註冊操做,存在在一個map結構中,更具體的請讀者往下看,歡迎指正不足的地方!web
1、同步、異步的概念redis
在學習多線程的時候,咱們接觸最多的概念估計是同步的概念了,多線程中同步的意思大概是這樣:線程訪問資源時一直在等待,知道資源訪問結束。因此,有同步的概念,咱們能夠大概理解與之相對的異步的概念:線程在訪問資源(或者處理耗時較長的數據)時,沒必要一直等待資源訪問完成或者數據處理完,在等待期間線程能夠作其餘事情,而當資源訪問完成以後,會採起回調的方式執行相應的代碼。spring
例如,在IO讀寫中,同步的方式就是在IO 操做的阻塞過程一直阻塞,直到IO操做完成;而異步的意思就是在io操做阻塞過程線程去作其餘事情,當IO操做完成後,採起回調的方式執行相應的操做。編程
2、異步框架的模型原理設計模式
一、生產者--消費者模式數據結構
有了解過設計模式的讀者應該聽過這個大名鼎鼎的設計模式了,它大概的思路以下示例圖:多線程
大概意思就是:生產者負責數據的產生,它將數據放到內存中去(通常是一個隊列),而消費者則負責處理內存中的數據,處理完成後,能夠經過回調的方式進行響應。上面的圖比較粗略,下面是具體的實現示意圖:app
上面示意圖具體說明了生產者消費者的具體實現方式:
eventProducer(固然,也能夠是dataProducer等)是生產者,它會將前端傳輸過來的數據或者說須要處理的事件封裝好,而後將這些封裝好的數據放進一個隊列裏面去;
而eventConsumer是消費者,它會讀取隊列裏面的數據,而後進行處理。
在這個過程,程序是以異步的方式運行的:生產者無需等待消費者處理完成,它的職責只是將數據推到內存裏面去,而後就能夠進行響應;而消費者只須要處理數據便可,它不用管數據是哪來的。顯然,這樣的方式能夠提升響應的速度,同時使得異步的實現方式變得簡單起來。
二、web開發中的異步框架思路
上面的生產者--消費者爲咱們實現web的異步框架提供了一種很好的思路:在複雜的業務操做或者耗時比較長的業務中,咱們能夠採用異步的方式提升程序的響應速度,而生產者消費者的模式正是咱們實現異步框架的參考模型--複雜業務的service層使對應的生產者,它只須要將要處理的數據放進一個隊列裏面,而後便可相應用戶;而相應的handler類則負責具體的數據處理。
三、爲何用異步?
顯然,在上面描述的思路中,咱們大概能夠知道何時應該使用異步框架:對相應速度要求比較高請求,可是該請求的相關業務操做容許必定的延遲。
舉個具體的例子:在一個社交網站中,不少時候會有點讚的操做,A給B點贊,通常來講會包含兩個操做,第一個操做是告訴A點同意功了,第二個操做是告訴B他被A點讚了;若是不採用異步的方式,那就須要在在這兩個操做都完成後,才響應A說點同意功,可是第二個操做顯然會耗時很長(例如須要發郵件通知),因此不採用異步方式時A就會有這樣一種感受:怎麼點個贊要等半天才響應的,什麼垃圾系統!因此,這時候爲了提升對A的相應速度,咱們能夠採用異步的方式:A點贊請求發出以後,程序不須要等到B收到A的點贊通知了,才告訴A說你點同意功了,由於B收到A的點贊通知相對於A知道本身點同意功來講,是容許延遲的。
好吧,上面的解說可能有點繞,不過若是你理解了上面的這個例子,大概也就知道異步的適用場景了。
3、簡單的事件處理異步框架
前面囉囉嗦嗦鋪墊了那麼多,下面就用一個比較簡單的例子來講明web開發中異步框架的應用場景以及如何實現一個簡單的異步框架吧。
首先說明的是,在下面的代碼中,我是將最近作的一個項目中的部分業務功能抽取出來的,因此會用到spring的框架以及redis(用於存儲生產者產生的數據)相關知識,同時爲了提升程序的擴展性,我採用了面向接口編程的方式,利用spring的內置功能實現消費者的自動註冊,看不懂能夠稍微百度下(其實只是用到了redis的一點皮毛功能,畢竟我也是剛接觸redis的菜鳥而已,因此不用擔憂看不懂)
一、框架的大致模型
主要是包括三個部分:生產者producer類,消費者comsumer類,事件處理的handler接口以及對應的實現類,具體的事件eventModel類(對應數據)。
在這裏,producer類會將前端傳輸過來的eventModel對象進行序列化,將它加入到一個異步隊列中,這裏採用redis的list數據結構實現。
消費者comsumer則負責將redis中隊列的數據讀取出來,反序列化後,根據eventModel中的eventType來調用相應的handler具體實現類(handler實現類存儲在一個map結構裏面,key對應的是eventType,value對應的是具體handler實現類)進行業務處理。
handler實現類負責具體事件的處理,它須要實現一個handler接口(該接口是經過spring進行自動註冊的關鍵,具體後面會講)。
eventModel是事件模型,它主要存儲與事件有關的數據,包括事件類型,時間觸發者,事件所屬者等數據。具體的後面會講解。
下面就各個模塊進行具體的講解以及給出相應的代碼實現。
二、eventModel事件模型
在講解其餘部分以前,我以爲首先應該簡單講解下咱們應該如何組織一個事件模型。直接上代碼吧,請注意看註釋理解如何組織事件模型:
/** * 事件模型:用於表示個事件 */ public class EventModel { /** * 事件類型,用於標識事件,同時在comsumer中根據這個值肯定handler的具體實現類,通常可用一個枚舉類型實現 * 例如點贊通知對應的事件類型和註冊發郵件進行激活的事件就應該屬於不一樣的eventType,應該對應不一樣的handler實現類 */ private EventType eventType; /** * 事件觸發者,例如用戶A給用戶B點贊,A就是時間觸發者 */ private int actionId;/** * 事件發生對應的關聯者,例如A給B點贊,A對應actionId,actionOwnerId */ private int actionOwnerId; /**時間處理須要的額外的數據,採用map的方式能夠保證程序的擴展性 * 例如註冊發送郵件的操做須要的數據和點贊通知須要的數據並不同,因此用map存儲最大程度地保證程序的靈活性 */ private Map<String,String> exts = new HashMap<>(); /** * 注意序列化須要顯式有一個無參構造函數 */ public EventModel(){ } /** * getter 和setter,這部分省略 */ }
在組織eventModel時,咱們應該保證靈活性,將必須的變量抽取出來之餘,用一個map結構來存儲具體業務可能須要的額外數據。
三、producer類
producer的功能較爲加單,只是將eventModel進行序列化,而後將它添加進相應的時間隊列,具體代碼以下:
/** * 事件生產者 */ @Component public class EventProducer { @Autowired private JedisEventHandlerAdaptor jedisEventHandlerAdaptor; @Autowired private JedisKeyUtil jedisKeyUtil; public void add(EventModel model){ String modelJson = JSONObject.toJSONString(model); jedisEventHandlerAdaptor.add(jedisKeyUtil.getEventHandlerKey(),modelJson); } }
沒有接觸過redis的讀者能夠認爲上面的jedisEventHandlerAdaptor其實就是一個能夠操做某個隊列的類,在java中其實也能夠用阻塞隊列來實現的,更具體的讀者能夠本身嘗試。
四、comsumer類
在這個異步事件處理框架中,comsumer主要負責如下的職責:
讀取事件隊列中的eventModel對象,將它反序列化後,根據eventType負責調用具體的handler實現類;
在初始化的時候利用spring框架自動對handler具體實現類進行註冊操做,並將之存儲在一個map的數據結構中,key是eventType,valuee是handler具體實現類的對象。
具體的實現方式請讀者注意看代碼中的註釋:
/** * 事件處理類,該類負責調用handler,對事件進行處理,須要實現spring的兩個接口,InitializingBean接口是初始化時自動註冊handler要用; *ApplicationContextAware則是調用spring的applicationContext(該applicationContext中存儲着handler具體實現類的bean對象)須要實現 * 的接口,經過applicationContext獲取handler對應的beans,而後就能夠將handler自動註冊到下面的config對象中了(是一個map) */ @Component public class EventComsumer implements InitializingBean, ApplicationContextAware{ private static final Logger logger = LoggerFactory.getLogger(MessageController.class); /** * threadPoolUtil封裝了線程池的線程相關操做 */ @Autowired private ThreadPoolUtil threadPool; @Autowired private JedisEventHandlerAdaptor adaptor; /** * 這是一個與redis交互相關的工具類,用於獲取特定的redis key,避免key衝突用,讀者能夠忽略 */ @Autowired private JedisKeyUtil jedisKeyUtil; /** * spring上下文對象,該對象存儲着handler bean對象,必須經過setApplicationContext(ApplicationContextAware接口的實現方法) * 進行初始化,這樣才能獲取spring中的handler具體實現類的beans */ private ApplicationContext applicationContext; /**設置消費函數阻塞時間,暫定爲一天,redis阻塞list中必需要的參數,讀者能夠忽略 */ private static int COMSUME_TIMEOUT = 24*3600; /** * congif:該變量用於存儲type和eventType的映射關係,在消費時,能夠直接根據config中你的映射關係進行handler調用 * 注意,這裏爲了保證程序的靈活性,eventHandler用一個list進行存儲,由於有可能一個EventType事件類型可能對應多個 * handler事件處理對象,例如點贊通知這個事件類型可能須要通知被點讚的人以及通知系統管理員,因此應該對應兩個事件handler * 更具體的能夠參考handler接口設計時的註釋 */ Map<EventType,List<EventHandler>> config = new HashMap<>(); /** * spring對該對象進行初始化的時候,將全部的handler具體對象註冊到config對象中 */ @Override public void afterPropertiesSet() throws Exception {
//獲取全部handler具體對象 Map<String,EventHandler> beans = applicationContext.getBeansOfType(EventHandler.class);
//迭代註冊handler對象 for(Map.Entry<String,EventHandler> entry:beans.entrySet()){ EventHandler handler = entry.getValue();
//因爲一個handler也可能對應多個事件類型,因此一個handler要註冊到全部的eventType中去,這裏若是看不懂能夠結合後面的解釋handler接口代碼的註釋進行理解 for(EventType type:handler.getHandlerType()){ if(config.get(type)==null){ config.put(type,new ArrayList<EventHandler>()); } config.get(type).add(handler); } } //開線程調用消費函數,注意不能直接調用,不然會致使主線程阻塞 threadPool.execute(new Runnable() { @Override public void run() { doConsume(); } }); } /** * 消費函數,用於執行handler */ public void doConsume() { while(true){ List<String> list = adaptor.pop(String.valueOf(COMSUME_TIMEOUT), jedisKeyUtil.getEventHandlerKey()); //反序列化 EventModel model = JSON.parseObject(list.get(1),EventModel.class); EventType type = model.getEventType(); //獲取事件的handler List<EventHandler> handlers = config.get(type); //執行handler for(EventHandler handler:handlers){ handler.doHandler(model); } } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
這裏須要重點解釋下comsumer中handler自動註冊的過程(afterPropertitiesSet方法以及config對象):
首先咱們的config對象是一個map,key是一個eventType,表示某個事件;而爲何用list來表示eventHandler呢?這是由於一個事件有可能對應多個eventHandler,因此爲了爲了保證靈活性,用list形式存儲handler最適合了;而關於handler方法中的getHandlerType方法,下面的handler接口設計講解時會進行詳細解釋。
另外,關於 InitializingBean, ApplicationContextAware 兩個接口的做用具體不講,讀者入股不知道爲何必須實現這兩個接口才能借助spring實現自動註冊的話,能夠進行谷歌或者百度下,相信很容易找到答案的。
五、handler接口設計
直接上代碼吧,請注意看註釋:
/** * 事件處理器:用於處理事件隊列裏面的事件,被eventConsumer調用 * doHandler:model是具體的事件模型,它須要由調用者(通常是comsumer)傳進來 */ public interface EventHandler { public void doHandler(EventModel model); /** * * @return 代表該接口是什麼類型的handler,list代表handler能夠支持多個業務,也就是說,一個handler能夠對應多個eventType
*例如說,sendEmailHandler,郵件發送handler,具體業務例如註冊激活的事件類型,點讚的發郵件通知時間類型都會須要這個handler,
*因此一個handler是有必要對應多個eventType的,這裏請讀者務必理解,當初我也理解了挺久的。因此,具體handler實現類中必須有一個list變量來存儲它對應的事件類型 */ public List<EventType> getHandlerType(); }
這裏的handler爲何要對應多個eventType請讀者參考註釋理解,我以爲理解這個挺重要的,當你理解這個以後,回頭看上面的自動註冊過程(在comsumer類中)纔不會感到懵逼。
最後,咱們只須要實現eventHandler接口就能夠了,comsumer會在spring啓動時自動幫你註冊該類,咱們只須要在service中聲明eventType,comsumer便會自動找到相應的接口執行具體操做。
六、總結
這個簡單的異步事假處理框架例子就大概解析到這裏了,其實我以爲最主要的是經過這個事件處理框架設計的過程體會和領悟生產者消費者設計模型以及異步框架的工做原理;固然,這個過程其實還有不少其餘須要領悟的:例如如何設計接口才能保證靈活性;對象的註冊又是什麼意思,咱們應該如何實現自動註冊等等。
用了一個早上終於把這篇博客寫完了,其實這個異步事件處理框架仍是有點粗糙的,可是在我開來再複雜的異步框架工做原理大致上也是這樣的,也但願這篇博客能給讀者帶來那麼一點點收穫,不足的地方請各位大佬指正!