1. 程式人生 > >圍觀:基於事件機制的內部解耦之心路歷程

圍觀:基於事件機制的內部解耦之心路歷程

每篇文章都有屬於它自己的故事,沒有故事的文章是沒有靈魂的文章。而我就是這個靈魂擺渡人。 主人公張某某,這邊不方便透露姓名,就叫小張吧。小張在一家小型的網際網路創業團隊中就職。 職位是Java後端開發,所以整體和業務程式碼打交道在所難免。 之前有個搜尋相關的需求,而且數量量也算比較大,就採用了ElasticSearch來做搜尋。第一版由於時間比較趕,做的比較粗糙。越到後面發現程式碼越難寫下去了,主要是在更新索引資料的場景沒處理好,才有了今天的故事。 # 基礎入門 ## Spring Event Spring的事件就是觀察者設計模式,一個任務結束後需要通知任務進行下一步的操作,就可以使用事件來驅動。 在Spring中使用事件機制的步驟如下: * 自定義事件物件,繼承 ApplicationEvent * 自定義事件監聽器,實現 ApplicationListener 或者通過 @EventListener 註解到方法上實現監聽 * 自定義釋出者,通過 applicationContext.publishEvent()釋出事件 Spring Event在很多開源框架中都有使用案例,比如Spring Cloud中的Eureka裡面就有使用 **event包** ![](https://img2020.cnblogs.com/blog/1618095/202003/1618095-20200331124729235-1969041313.png) **定義Event** ![](https://img2020.cnblogs.com/blog/1618095/202003/1618095-20200331124738076-869769596.png) **釋出Event** ![](https://img2020.cnblogs.com/blog/1618095/202003/1618095-20200331124746224-1085502941.png) ## Guava EventBus EventBus是Guava的事件處理機制,在使用層面和Spring Event差不多。這裡不做過多講解,今天主要講Spring Event。 # 業務背景 所有的資料都會有一個定時任務去同步資料到ElasticSearch中,業務中直接從ElasticSearch查詢資料返回給呼叫方。 之所以把所有資料都存入ElasticSearch是為了方便,如果只儲存搜尋的欄位,那麼搜尋出來後就還需要去資料庫查詢其他資訊進行組裝。 就是由於所有資料都會儲存ElasticSearch中,所以當這些資料發生變更的時候我們需要去重新整理ElasticSearch中的資料,這個就是我們今天文章的核心背景。 假設我們ElasticSearch中的資料是文章資訊,也就是我們經常看的技術文章,這個文章中儲存了訪問量,點贊量,評論量等資訊。 當這些動作發生的時候,都需要去更新ElasticSearch的資料才行,我們預設的操作都是更新資料庫中的資料,ElasticSearch是由定時任務去同步的,同步會有周期,做不到毫秒別更新。 # 實現方案-倔強青銅 倔強青銅就是在每個會涉及到資料變更的地方,去手動呼叫程式碼進行資料的重新整理操作,弊端在於每個地方都要去呼叫,這還是簡單的場景,有複雜的業務場景,一個業務操作可能會涉及到很多資料的重新整理,也就是需要呼叫很多次,模擬程式碼如下: ``` // 瀏覽 public void visit() { articleIndexService.reIndex(articleId); XXXIndexService.reIndex(articleId); ........ } // 評論 public void comment() { articleIndexService.reIndex(articleId); } ``` # 實現方案-秩序白銀 倔強青銅的弊端在於不解耦,而且是同步呼叫,如果在事務中會加長事務的時間。所以我們需要一個非同步的方案來執行重建索引的邏輯。 經過大家激烈的討論,而專案也是以Spring Boot為主,所以選擇了Spring Event來作為非同步方案。 定義一個重建文章索引的Event,程式碼如下: ``` public class ArticleReIndexEvent extends ApplicationEvent { private String id; public ArticleReIndexEvent(Object source, String id) { super(source); this.id = id; } public String getId() { return id; } } ``` 然後寫一個EventListener來監聽事件,進行業務邏輯處理,程式碼如下: ``` @Component public class MyEventListener { @EventListener public void onEvent(ArticleReIndexEvent event) { System.out.println(event.getId()); } } ``` 使用的地方只需要釋出一個Event就可以,這個動作預設是同步的,如果我們想讓這個操作不會阻塞,變成非同步只需要在@EventListener上面再增加一個@Async註解。 ``` // 瀏覽 public void visit() { applicationContext.publishEvent(new ArticleReIndexEvent(this, articleId)); applicationContext.publishEvent(new XXXReIndexEvent(this, articleId)); } // 評論 public void comment() { applicationContext.publishEvent(new ArticleReIndexEvent(this, articleId)); } ``` # 實現方案-榮耀黃金 秩序白銀的方案在程式碼層面確實解耦了,但是使用者釋出事件需要關注的點太多了,也就是我改了某個表的資料,我得知道有哪些索引會用到這張表的資料,我得把這些相關的事件都發送出去,這樣資料才會非同步進行重新整理。 當業務複雜後或者有新來的同事,不是那麼的瞭解業務,壓根不可能知道說我改了這個資料對其他那些索引有影響,所以這個方案還是有優化的空間。 榮耀黃金的方案是將所有的事件都統一為一個,然後在事件里加屬性來區分修改的資料是哪裡的。每個資料需要同步變更的索引都有自己的監聽器,去監聽這個統一的事件,這樣對於釋出者來說我只需要傳送一個事件告訴你,我這邊改資料了,你要不要消費,要不要更新索引我並不關心。 定義一個數據表發生修改的事件,程式碼如下: ``` public class TableUpdateEvent extends ApplicationEvent { private String table; private String id; public TableUpdateEvent(Object source, String id, String table) { super(source); this.id = id; this.table = table; } public String getId() { return id; } public String getTable() { return table; } } ``` 然後每個索引都需要消費這個事件,只需要關注這個索引中資料的來源表有沒有變動,如果有變動則去重新整理索引。 比如索引A的資料都是article表中過來的,所以只要是article表中的資料發生了變更,索引A都要做對應的處理,所以索引A的監聽器只需要關注article表有沒有修改即可。 ``` @Component public class MyEventListener { priv