1. 程式人生 > >多線程實現簡單的事件異步處理框架

多線程實現簡單的事件異步處理框架

void 序列化 以及 ise 包含 反序列化 高程 結合 映射

老實說,多線程在web開發裏面非常常見,很多web容器本身就支持多線程,所以很多時候我們在進行web開發的時候並不需要考慮多線程相關的負責問題,而只需要實現相關的業務功能即可。所以,可以概括地講,很多時候的web開發,並沒有多線程方面的考慮,因為web應用本身就是在多線程基礎上的了。

但是,有些時候為了提高程序性能,在用戶的一個請求中中如果包含過多的業務操作或者包含耗時比較長的業務操作,我們就需要考慮使用異步的方式來提高程序響應的速度了。這篇博客簡單介紹了在java中如何使用多線程實現一個簡單的異步框架。

這個事件異步處理框架主要的工作過程是這樣的:通過producer類對事件實體類序列化後,存儲在redis的list隊列中,而comsumer則負責讀取事件隊列中的事件模型對象並反序列化後調用相應的handler實現類對象進行事件處理,這些handler實現類的對象,通過spring完成handler具體類的註冊操作,存在在一個map結構中,更具體的請讀者往下看,歡迎指正不足的地方!

一、同步、異步的概念

  在學習多線程的時候,我們接觸最多的概念估計是同步的概念了,多線程中同步的意思大概是這樣:線程訪問資源時一直在等待,知道資源訪問結束。所以,有同步的概念,我們可以大概理解與之相對的異步的概念:線程在訪問資源(或者處理耗時較長的數據)時,不必一直等待資源訪問完成或者數據處理完,在等待期間線程可以做其他事情,而當資源訪問完成之後,會采取回調的方式執行相應的代碼。

  例如,在IO讀寫中,同步的方式就是在IO 操作的阻塞過程一直阻塞,直到IO操作完成;而異步的意思就是在io操作阻塞過程線程去做其他事情,當IO操作完成後,采取回調的方式執行相應的操作。

二、異步框架的模型原理

  1、生產者--消費者模式

    有了解過設計模式的讀者應該聽過這個大名鼎鼎的設計模式了,它大概的思路如下示例圖:

技術分享

大概意思就是:生產者負責數據的產生,它將數據放到內存中去(一般是一個隊列),而消費者則負責處理內存中的數據,處理完成後,可以通過回調的方式進行響應。上面的圖比較粗略,下面是具體的實現示意圖:

技術分享

上面示意圖具體說明了生產者消費者的具體實現方式:

eventProducer(當然,也可以是dataProducer等)是生產者,它會將前端傳輸過來的數據或者說需要處理的事件封裝好,然後將這些封裝好的數據放進一個隊列裏面去;

而eventConsumer是消費者,它會讀取隊列裏面的數據,然後進行處理。

在這個過程,程序是以異步的方式運行的:生產者無需等待消費者處理完成,它的職責只是將數據推到內存裏面去,然後就可以進行響應;而消費者只需要處理數據即可,它不用管數據是哪來的。顯然,這樣的方式可以提高響應的速度,同時使得異步的實現方式變得簡單起來。

  2、web開發中的異步框架思路

  上面的生產者--消費者為我們實現web的異步框架提供了一種很好的思路:在復雜的業務操作或者耗時比較長的業務中,我們可以采用異步的方式提高程序的響應速度,而生產者消費者的模式正是我們實現異步框架的參考模型--復雜業務的service層使對應的生產者,它只需要將要處理的數據放進一個隊列裏面,然後即可相應用戶;而相應的handler類則負責具體的數據處理。

  3、為什麽用異步?

  顯然,在上面描述的思路中,我們大概可以知道什麽時候應該使用異步框架:對相應速度要求比較高請求,但是該請求的相關業務操作允許一定的延遲。

  舉個具體的例子:在一個社交網站中,很多時候會有點贊的操作,A給B點贊,一般來說會包含兩個操作,第一個操作是告訴A點贊成功了,第二個操作是告訴B他被A點贊了;如果不采用異步的方式,那就需要在在這兩個操作都完成後,才響應A說點贊成功,但是第二個操作顯然會耗時很長(例如需要發郵件通知),所以不采用異步方式時A就會有這樣一種感覺:怎麽點個贊要等半天才響應的,什麽垃圾系統!所以,這時候為了提高對A的相應速度,我們可以采用異步的方式:A點贊請求發出之後,程序不需要等到B收到A的點贊通知了,才告訴A說你點贊成功了,因為B收到A的點贊通知相對於A知道自己點贊成功來說,是允許延遲的。

  好吧,上面的解說可能有點繞,不過如果你理解了上面的這個例子,大概也就知道異步的適用場景了。

三、簡單的事件處理異步框架

  前面啰啰嗦嗦鋪墊了那麽多,下面就用一個比較簡單的例子來說明web開發中異步框架的應用場景以及如何實現一個簡單的異步框架吧。

  首先說明的是,在下面的代碼中,我是將最近做的一個項目中的部分業務功能抽取出來的,所以會用到spring的框架以及redis(用於存儲生產者產生的數據)相關知識,同時為了提高程序的擴展性,我采用了面向接口編程的方式,利用spring的內置功能實現消費者的自動註冊,看不懂可以稍微百度下(其實只是用到了redis的一點皮毛功能,畢竟我也是剛接觸redis的菜鳥而已,所以不用擔心看不懂)

  1、框架的大體模型

  主要是包括三個部分:生產者producer類,消費者comsumer類,事件處理的handler接口以及對應的實現類,具體的事件eventModel類(對應數據)。

  在這裏,producer類會將前端傳輸過來的eventModel對象進行序列化,將它加入到一個異步隊列中,這裏采用redis的list數據結構實現。

  消費者comsumer則負責將redis中隊列的數據讀取出來,反序列化後,根據eventModel中的eventType來調用相應的handler具體實現類(handler實現類存儲在一個map結構裏面,key對應的是eventType,value對應的是具體handler實現類)進行業務處理。

  handler實現類負責具體事件的處理,它需要實現一個handler接口(該接口是通過spring進行自動註冊的關鍵,具體後面會講)。

  eventModel是事件模型,它主要存儲與事件有關的數據,包括事件類型,時間觸發者,事件所屬者等數據。具體的後面會講解。

  下面就各個模塊進行具體的講解以及給出相應的代碼實現。

  2、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結構來存儲具體業務可能需要的額外數據。

  3、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中其實也可以用阻塞隊列來實現的,更具體的讀者可以自己嘗試。

  4、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實現自動註冊的話,可以進行谷歌或者百度下,相信很容易找到答案的。

  5、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便會自動找到相應的接口執行具體操作。

  6、總結

    這個簡單的異步事假處理框架例子就大概解析到這裏了,其實我覺得最主要的是通過這個事件處理框架設計的過程體會和領悟生產者消費者設計模型以及異步框架的工作原理;當然,這個過程其實還有很多其他需要領悟的:例如如何設計接口才能保證靈活性;對象的註冊又是什麽意思,我們應該如何實現自動註冊等等。

  

  用了一個早上終於把這篇博客寫完了,其實這個異步事件處理框架還是有點粗糙的,但是在我開來再復雜的異步框架工作原理大體上也是這樣的,也希望這篇博客能給讀者帶來那麽一點點收獲,不足的地方請各位大佬指正!

  

  

 

多線程實現簡單的事件異步處理框架