一文讀懂Guava EventBus(訂閱\釋出事件)

京東雲開發者發表於2023-02-22
作者:京東科技 劉子洋

背景

最近專案出現同一訊息傳送多次的現象,對下游業務方造成困擾,經過排查發現使用EventBus方式不正確。也藉此機會學習了下EventBus並進行分享。以下為分享內容,本文主要分為五個部分,篇幅較長,望大家耐心閱讀。

  • 1、簡述:簡單介紹EventBus及其組成部分。
  • 2、原理解析:主要對listener註冊流程及Event釋出流程進行解析。
  • 3、使用指導:EventBus簡單的使用指導。
  • 4、注意事項:在使用EventBus中需要注意的一些隱藏邏輯。
  • 5、分享時提問的問題
  • 6、專案中遇到的問題:上述問題進行詳細描述並復現場景。

1、簡述

1.1、概念

下文摘自EventBus原始碼註釋,從註釋中可以直觀瞭解到他的功能、特性、注意事項

【原始碼註釋】

Dispatches events to listeners, and provides ways for listeners to register themselves.

The EventBus allows publish-subscribe-style communication between components without requiring the components to explicitly register with one another (and thus be aware of each other). It is designed exclusively to replace traditional Java in-process event distribution using explicit registration. It is not a general-purpose publish-subscribe system, nor is it intended for interprocess communication.

Receiving Events

To receive events, an object should:

  • Expose a public method, known as the event subscriber, which accepts a single argument of the type of event desired;
  • Mark it with a Subscribe annotation;
  • Pass itself to an EventBus instance's register(Object) method.

Posting Events

To post an event, simply provide the event object to the post(Object) method. The EventBus instance will determine the type of event and route it to all registered listeners.

Events are routed based on their type — an event will be delivered to any subscriber for any type to which the event is assignable. This includes implemented interfaces, all superclasses, and all interfaces implemented by superclasses.

When post is called, all registered subscribers for an event are run in sequence, so subscribers should be reasonably quick. If an event may trigger an extended process (such as a database load), spawn a thread or queue it for later. (For a convenient way to do this, use an AsyncEventBus.)

Subscriber Methods

Event subscriber methods must accept only one argument: the event.

Subscribers should not, in general, throw. If they do, the EventBus will catch and log the exception. This is rarely the right solution for error handling and should not be relied upon; it is intended solely to help find problems during development.

The EventBus guarantees that it will not call a subscriber method from multiple threads simultaneously, unless the method explicitly allows it by bearing the AllowConcurrentEvents annotation. If this annotation is not present, subscriber methods need not worry about being reentrant, unless also called from outside the EventBus.

Dead Events

If an event is posted, but no registered subscribers can accept it, it is considered "dead." To give the system a second chance to handle dead events, they are wrapped in an instance of DeadEvent and reposted.

If a subscriber for a supertype of all events (such as Object) is registered, no event will ever be considered dead, and no DeadEvents will be generated. Accordingly, while DeadEvent extends Object, a subscriber registered to receive any Object will never receive a DeadEvent.

This class is safe for concurrent use.

See the Guava User Guide article on EventBus.
Since:
10.0
Author:
Cliff Biffle

1.2、系統流程

image.png

1.3、組成部分

image.png

1.3.1、排程器

EventBus、AsyncEventBus都是一個排程的角色,區別是一個同步一個非同步。

  • EventBus
原始碼註釋:
> Dispatches events to listeners, and provides ways for listeners to register themselves.

意思是說EventBus分發事件(Event)給listeners處理,並且提供listeners註冊自己的方法。從這裡我們可以看出EventBus主要是一個排程的角色。

**EventBus總結**
- 1.同步執行,事件傳送方在發出事件之後,會等待所有的事件消費方執行完畢後,才會回來繼續執行自己後面的程式碼。
- 2.事件傳送方和事件消費方會在同一個執行緒中執行,消費方的執行執行緒取決於傳送方。
- 3.同一個事件的多個訂閱者,在接收到事件的順序上面有不同。誰先註冊到EventBus的,誰先執行,如果是在同一個類中的兩個訂閱者一起被註冊到EventBus的情況,收到事件的順序跟方法名有關。
  • AsyncEventBus
原始碼註釋:
> An {@link EventBus} that takes the Executor of your choice and uses it to dispatch events, allowing dispatch to occur asynchronously.

意思是說AsyncEventBus就是EventBus,只不過AsyncEventBus使用你指定的執行緒池(不指定使用預設執行緒池)去分發事件(Event),並且是非同步進行的。

**AsyncEventBus總結**
- 1.非同步執行,事件傳送方非同步發出事件,不會等待事件消費方是否收到,直接執行自己後面的程式碼。
- 2.在定義AsyncEventBus時,建構函式中會傳入一個執行緒池。事件消費方收到非同步事件時,消費方會從執行緒池中獲取一個新的執行緒來執行自己的任務。
- 3.同一個事件的多個訂閱者,它們的註冊順序跟接收到事件的順序上沒有任何聯絡,都會同時收到事件,並且都是在新的執行緒中,**非同步併發**的執行自己的任務。

1.3.2、事件承載器

  • Event
事件主體,用於承載訊息。
  • DeadEvent
 原始碼註釋:
>Wraps an event that was posted, but which had no subscribers and thus could not be delivered, Registering a DeadEvent subscriber is useful for debugging or logging, as it can detect misconfigurations in a system's event distribution.

意思是說DeadEvent就是一個被包裝的event,只不過是一個沒有訂閱者無法被分發的event。我們可以在開發時註冊一個DeadEvent,因為它可以檢測系統事件分佈中的錯誤配置。

1.3.3、事件註冊中心

SubscriberRegistry

 原始碼註釋:
>  Registry of subscribers to a single event bus.
意思是說SubscriberRegistry是單個事件匯流排(EventBus)的訂閱者登錄檔。

1.3.4、事件分發器

Dispatcher

原始碼註釋:
>Handler for dispatching events to subscribers, providing different event ordering guarantees that make sense for different situations.

>Note: The dispatcher is orthogonal to the subscriber's Executor. The dispatcher controls the order in which events are dispatched, while the executor controls how (i.e. on which thread) the subscriber is actually called when an event is dispatched to it.

意思是說Dispatcher主要任務是將事件分發到訂閱者,並且可以不同的情況,按不同的順序分發。

Dispatcher有三個子類,用以滿足不同的分發情況

1.PerThreadQueuedDispatcher

原始碼註釋:
> Returns a dispatcher that queues events that are posted reentrantly on a thread that is already dispatching an event, guaranteeing that all events posted on a single thread are dispatched to all subscribers in the order they are posted.

> When all subscribers are dispatched to using a direct executor (which dispatches on the same thread that posts the event), this yields a breadth-first dispatch order on each thread. That is, all subscribers to a single event A will be called before any subscribers to any events B and C that are posted to the event bus by the subscribers to A.

意思是說一個執行緒在處理事件過程中又釋出了一個事件,PerThreadQueuedDispatcher會將後面這個事件放到最後,從而保證在單個執行緒上釋出的所有事件都按其釋出順序分發給訂閱者。**注意,每個執行緒都要自己儲存事件的佇列。**

第二段是說PerThreadQueuedDispatcher按**廣度優先**分發事件。並給了一個例子:
程式碼中釋出了事件A,訂閱者收到後,在執行過程中又釋出了事件B和事件C,PerThreadQueuedDispatcher會確保事件A分發給所有訂閱者後,再分發B、C事件。

2.LegacyAsyncDispatcher

原始碼註釋:
> Returns a dispatcher that queues events that are posted in a single global queue. This behavior matches the original behavior of AsyncEventBus exactly, but is otherwise not especially useful. For async dispatch, an immediate dispatcher should generally be preferable.

意思是說LegacyAsyncDispatcher有一個全域性佇列用於存放所有事件,LegacyAsyncDispatcher特性與AsyncEventBus特性完全相符,除此之外沒有其他什麼特性。如果非同步分發的話,最好用immediate dispatcher。

3.ImmediateDispatcher

原始碼註釋:
> Returns a dispatcher that dispatches events to subscribers immediately as they're posted without using an intermediate queue to change the dispatch order. This is effectively a depth-first dispatch order, vs. breadth-first when using a queue.

意思是說ImmediateDispatcher在釋出事件時立即將事件分發給訂閱者,而不使用中間佇列更改分發順序。這實際上是**深度優先**的排程順序,而不是使用佇列時的**廣度優先**。

1.3.4、訂閱者

  • Subscriber
原始碼註釋:
> A subscriber method on a specific object, plus the executor that should be used for dispatching events to it.

Two subscribers are equivalent when they refer to the same method on the same object (not class). This property is used to ensure that no subscriber method is registered more than once.

第一段意思是說,Subscriber是特定物件(Event)的訂閱方法,用於執行被分發事件。第二段說當兩個訂閱者在同一物件 **(不是類)** 上引用相同的方法時,它們是等效的,此屬性用於確保不會多次註冊任何訂閱者方法,主要說明會對訂閱者進行判重,如果是同一個物件的同一個方法,則認為是同一個訂閱者,不會進行重複註冊。
  • SynchronizedSubscriber
原始碼註釋:
> Subscriber that synchronizes invocations of a method to ensure that only one thread may enter the method at a time.

意思是說同步方法呼叫以確保一次只有一個執行緒可以執行訂閱者方法(執行緒安全)。

2、原理解析

2.1、主體流程

  1. listener 透過EventBus進行註冊。
  2. SubscriberRegister 會根據listener、listener中含有【@Subscribe】註解的方法及各方法引數建立Subscriber 物件,並將其維護在Subscribers(ConcurrentMap型別,key為event類物件,value為subscriber集合)中。
  3. publisher釋出事件Event。
  4. 釋出Event後,EventBus會從SubscriberRegister中查詢出所有訂閱此事件的Subscriber,然後讓Dispatcher分發Event到每一個Subscriber。

流程如下:

image.png

2.2、listener註冊原理

2.2.1、listener註冊流程

  1. 快取所有含有@Subscribe註解方法到subscriberMethodsCache(LoadingCache<Class<?>, ImmutableList>, key為listener,value為method集合)。
  2. listener註冊。

image.png

2.2.2、原理分析

  • 獲取含有@Subscribe註釋的方法進行快取
    找到所有被【@Subscribe】修飾的方法,並進行快取
    注意!!!這兩個方法被static修飾,類載入的時候就進行尋找
    image.png
    image.png

訂閱者唯一標識是【方法名+入參】
image.png

  • 註冊訂閱者
    1.註冊方法
    image.png
    建立Subscriber時,如果method含有【@AllowConcurrentEvents】註釋,則建立SynchronizedSubscriber,否則建立Subscriber
    image.png
    2、獲取所有訂閱者
    image.png
    3、從快取中獲取所有訂閱方法
    image.png

2.3、Event釋出原理

2.3.1、釋出主體流程

  • publisher 釋出事件Event。
  • EventBus 根據Event 類物件從SubscriberRegistry中獲取所有訂閱者。
  • 將Event 和 eventSubscribers 交由Dispatcher去分發。
  • Dispatcher 將Event 分發給每個Subscribers。
  • Subscriber 利用反射執行訂閱者方法。

圖中畫出了三個Dispatcher的分發原理。
image.png

2.3.2、原理分析

  • 建立快取
    快取EventMsg所有超類
    注意!!!此處是靜態方法,因此在程式碼載入的時候就會快取Event所有超類。
    image.png
  • 釋出Event事件
    此方法是釋出事件時呼叫的方法。
    image.png
  • 獲取所有訂閱者
    1、從快取中獲取所有訂閱者
    image.png
    2、獲取Event超類
    image.png
  • 事件分發
    1、分發入口
    image.png
    2、分發器分發
    2.1、ImmediateDispatcher
    來了一個事件則通知對這個事件感興趣的訂閱者。
    image.png
    2.2、PerThreadQueuedDispatcher(EventBus預設選項)
    在同一個執行緒post的Event執行順序是有序的。用ThreadLocal<Queue> queue來實現每個執行緒的Event有序性,在把事件新增到queue後會有一個ThreadLocal dispatching來判斷當前執行緒是否正在分發,如果正在分發,則這次新增的event不會馬上進行分發而是等到dispatching的值為false(分發完成)才進行。
    原始碼如下:
    image.png
    image.png
    2.3、LegacyAsyncDispatcher(AsyncEventBus預設選項)
    會有一個全域性的佇列ConcurrentLinkedQueue queue儲存EventWithSubscriber(事件和subscriber),如果被不同的執行緒poll,不能保證在queue佇列中的event是有序釋出的。原始碼如下:
    image.png
    image.png
  • 執行訂閱者方法
    方法入口是dispatchEvent,原始碼如下:
    image.png
    由於Subscriber有兩種,因此執行方法也有兩種:
    1.Subscriber(非執行緒安全)
    image.png
    2.SynchronizedSubscriber(執行緒安全)
    注意!!!執行方法會加同步鎖
    image.png

3、使用指導

3.1、主要流程

image.png

3.2、流程詳解

  • 1、建立EventBus、AsyncEventBus Bean
    在專案中統一配置全域性單例Bean(如特殊需求,可配置多例)
    image.png
  • 2、定義EventMsg
    設定訊息載體。
    image.png
  • 3、註冊Listener
    註冊Listener,處理事件
    image.png
    注意! 在使用 PostConstruct註釋進行註冊時,需要注意子類會執行父類含有PostConstruct 註釋的方法。
  • 3、事件釋出
    封裝統一發布事件的Bean,然後透過Bean注入到需要釋出的Bean裡面進行事件釋出。
    image.png
    image.png

此處對EventBus進行了統一封裝收口操作,主要考慮的是如果做一些操作,直接改這一處就可以。如果不需要封裝,可以在使用的地方直接注入EventBus即可。

4、注意事項

4.1、迴圈分發事件

如果業務流程較長,切記梳理好業務流程,不要讓事件迴圈分發。
目前EventBus沒有對迴圈事件進行處理。

4.2、使用 @PostConstrucrt 註冊listener

子類在執行例項化時,會執行父類@PostConstrucrt 註釋。 如果listenerSon繼承listenerFather,當兩者都使用@PostConstrucrt註冊訂閱方法時,子類也會呼叫父類的註冊方法進行註冊訂閱方法。由於EventBus機制,子類註冊訂閱方法時,也會註冊父類的監聽方法
image.png
Subscriber唯一標誌是(listener+method),因此在對同一方法註冊時,由於不是同一個listener,所以對於EventBus是兩個訂閱方法。
image.png
image.png
image.png

因此,如果存在listenerSon、listenerFather兩個listener,且listenerSon繼承listenerFather。當都使用@PostConstrucrt註冊時,會導致listenerFather裡面的訂閱方法註冊兩次。

4.3、含有繼承關係的listener

當註冊listener含有繼承關係時,listener處理Event訊息時,listener的父類也會處理該訊息。

4.3.1、繼承關係的訂閱者

image.png

4.3.2、原理

子類listener註冊,父類listener也會註冊
image.png

4.4、含有繼承關係的Event

如果作為引數的Event有繼承關係,使用EventBus釋出Event時,Event父類的監聽者也會對Event進行處理。

4.4.1、執行結果

image.png
image.png

4.4.2、原理

在分發訊息的時候,會獲取所有訂閱者資料(Event訂閱者和Event超類的訂閱者),然後進行分發資料。
獲取訂閱者資料如下圖:
image.png

image.png

快取Event及其超類的類物件,key為Event類物件。
image.png

5、分享提問問題

問題1:PerThreadQueuedDispatcherd 裡面的佇列,是否是有界佇列?

有界佇列,最大值為 int 的最大值 (2147483647),原始碼如下圖:

image.png

image.png

image.png

image.png

image.png

image.png

問題2:dispatcher 分發給訂閱者是否有序?

EventBus:同步事件匯流排
同一個事件的多個訂閱者,在接收到事件的順序上面有不同。誰先註冊到EventBus的,誰先執行(由於base使用的是PostConstruct進行註冊,因此跟不同Bean之間的初始化順序有關係)。如果是在同一個類中的兩個訂閱者一起被註冊到EventBus的情況,收到事件的順序跟方法名有關。

AsyncEventBus:非同步事件匯流排:同一個事件的多個訂閱者,它們的註冊順序跟接收到事件的順序上沒有任何聯絡,都會同時收到事件,並且都是在新的執行緒中,非同步併發的執行自己的任務。

問題3:EventBus與SpringEvent的對比?
  • 使用方式比較
專案事件釋出者釋出方法是否非同步監聽者註冊方式
EventBus任意物件EventBusEventBus#post支援同步非同步註解Subscribe方法手動註冊EventBus#register
SpringEvent任意物件ApplicationEventPublisherApplicationEventPublisher#publishEvent支援同步非同步註解EventListener方法系統註冊
  • 使用場景比較
專案事件區分是否支援事件簇是否支援自定義event是否支援過濾是否支援事件隔離是否支援事務是否支援設定訂閱者消費順序複雜程度
EventBusClass簡單
Spring EventClass複雜

參考連結https://www.cnblogs.com/shoren/p/eventBus_springEvent.html

問題4:EventBus的使用場景,結合現有使用場景考慮是否合適?

EventBus暫時不適用,主要有一下幾個點:

  • EventBus不支援事務,專案在更新、建立商品時,最好等事務提交成功後,再傳送MQ訊息(主要問題點)
  • EventBus不支援設定同一訊息的訂閱者消費順序。
  • EventBus不支援訊息過濾。SpringEvent支援訊息過濾

6.專案中遇到的問題

6.1、問題描述

商品上架時會觸發渠道分發功能,會有兩步操作

  • 1、建立一條分發記錄,並對外傳送一條未分發狀態的商品變更訊息(透過eventBus 事件傳送訊息)。
  • 2、將分發記錄改為稽核中(需要稽核)或稽核透過(不需要稽核),並對外傳送一條已分發狀態的商品變更訊息(透過eventBus 事件傳送訊息)。

所以分發會觸發兩條分發狀態不同的商品變更訊息,一條是未分發,另一條是已分發。實際傳送了兩條分發狀態相同的商品變更訊息,狀態都是已分發

6.2、原因

我們先來回顧下EventBus 監聽者處理事件時有三種策略,這是根本原因:

  • ImmediateDispatcher:來一個事件馬上進行處理。
  • PerThreadQueuedDispatcher(eventBus預設選項,專案中使用此策略):在同一個執行緒post的Event,執行的順序是有序的。用ThreadLocal<Queue> queue來實現每個執行緒post的Event是有序的,在把事件新增到queue後會有一個ThreadLocal dispatching來判斷當前執行緒是否正在分發,如果正在分發,則這次新增的event不會馬上進行分發而是等到dispatching的值為false才進行。
  • LegacyAsyncDispatcher(AsyncEventBus預設選項):會有一個全域性的佇列ConcurrentLinkedQueue queue儲存EventWithSubscriber(事件和subscriber),如果被不同的執行緒poll 不能保證在queue佇列中的event是有序釋出的。

詳情可見上文中的【2.3.4、事件分發】

再看下專案中的邏輯:

商品自動分發在商品變更的Listener裡操作。

由於當前分發操作處於商品上架事件處理過程中,因此對於新增分發記錄事件不會立馬處理,而是將其放入佇列。

上架操作完成,分發狀態變為已分發。

等上架操作完成後,商品變更Listener處理分發事件(此時有兩條EventMsg,一個是新增分發記錄另一個是修改分發狀態),分發狀態實時查詢,對於第一個分發事件,查詢到的分發記錄是已分發狀態。

最終導致兩條訊息都是已分發狀態。

6.3、場景復現

在handler中對靜態變數進行兩次+1 操作,每操作一步傳送一條事件,此處假設靜態變數為分發狀態。
image.png
image.png

6.4、解決辦法

目前 Dispatcher 包用default 修飾,使用者無法指定Dispatcher 策略。並且 ImmediateDispatcher 使用private修飾。
image.png
image.png
image.png

因此目前暫無解決非同步問題,只能在業務邏輯上進行規避。

其實可以修改原始碼併釋出一個包自己使用,但是公司安全規定不允許這樣做,只能透過業務邏輯上進行規避,下圖是github上對此問題的討論。
image.png

7、總結

如果專案中需要使用非同步解耦處理一些事項,使用EventBus還是比較方便的。

相關文章