SOFA 原始碼分析— 事件匯流排

莫那·魯道發表於2019-02-15
SOFA 原始碼分析— 事件匯流排

前言

大部分框架都是事件訂閱功能,即觀察者模式,或者叫事件機制。通過訂閱某個事件,當觸發事件時,回撥某個方法。該功能非常的好用,而 SOFA 內部也設計了這個功能,並且內部大量使用了該功能。來看看是如何設計的。

原始碼分析

核心類有 3 個:

  • EventBus 事件匯流排
  • Event 事件,即被觀察者
  • Subscriber 訂閱者,即觀察者

Subscriber 是個抽象類, 子類需要自己實現 onEvent 方法,即回撥方法。還有一個是否同步執行的引數。

EventBus 類實現了註冊功能,反註冊功能(刪除)。事件發生時通知訂閱者功能。

內部使用一個“大型資料結構”儲存事件和訂閱者的資訊。

ConcurrentHashMap<Class<? extends Event>, CopyOnWriteArraySet<Subscriber>> SUBSCRIBER_MAP
複製程式碼

所有相關資訊都儲存在該資料結構中。

看看註冊功能。

public static void register(Class<? extends Event> eventClass, Subscriber subscriber) {
    CopyOnWriteArraySet<Subscriber> set = SUBSCRIBER_MAP.get(eventClass);
    if (set == null) {
        set = new CopyOnWriteArraySet<Subscriber>();
        CopyOnWriteArraySet<Subscriber> old = SUBSCRIBER_MAP.putIfAbsent(eventClass, set);
        if (old != null) {
            set = old;
        }
    }
    set.add(subscriber);
}
複製程式碼

引數為 一個事件物件,一個訂閱物件。

首先從 Map 中根據事件的 Class 獲取對應的訂閱者集合,注意,這裡都是用的併發容器。

下面的判斷有點意思,考慮到併發的情況,如果第一次獲取 Set 是 null,則嘗試建立一個並放進 Map,這裡使用的並不是 put 方法,而是 putIfAbsent 方法,該方法作用等同於:

  if (!map.containsKey(key)) 
      return map.put(key, value);
  else
       return map.get(key);
複製程式碼

所以,這裡再一次考慮併發問題,如果這個間隙有其他執行緒 put 了,就可以獲取到那個執行緒 put 的 Set。很謹慎。而且效能相比較鎖要好很多。雖然這個方法併發量不會很高,但也是一種效能優化。

如果發生了併發,就使用已有的 Set,然後將 Set 放置到 Map 中,完成事件和訂閱者的對映。

再看看取消註冊方法。

public static void unRegister(Class<? extends Event> eventClass, Subscriber subscriber) {
    CopyOnWriteArraySet<Subscriber> set = SUBSCRIBER_MAP.get(eventClass);
    if (set != null) {
        set.remove(subscriber);
    }
}
複製程式碼

很簡單,就是直接刪除。

再看看通知功能:

    public static void post(final Event event) {
        if (!isEnable()) {
            return;
        }
        CopyOnWriteArraySet<Subscriber> subscribers = SUBSCRIBER_MAP.get(event.getClass());
        if (CommonUtils.isNotEmpty(subscribers)) {
            for (final Subscriber subscriber : subscribers) {
                if (subscriber.isSync()) {
                    handleEvent(subscriber, event);
                } else { // 非同步
                    AsyncRuntime.getAsyncThreadPool().execute(
                        new Runnable() {
                            @Override
                            public void run() {
                                handleEvent(subscriber, event);
                            }
                        });
                }
            }
        }
    }
複製程式碼

首先看是否開啟了匯流排功能,在效能測試的時候,可能是關閉的。

如果開啟了,就根據給定的時間找到訂閱者,迴圈呼叫 handleEvent 方法(其實就是呼叫訂閱者的 onEvent 方法)。

這裡有一個是否非同步的判斷,如果非同步的,則在非同步執行緒池執行。

這個非同步執行緒池 AsyncRuntime 可以看一下:

public static ThreadPoolExecutor getAsyncThreadPool(boolean build) {
    if (asyncThreadPool == null && build) {
        synchronized (AsyncRuntime.class) {
            if (asyncThreadPool == null && build) {
                // 一些系統引數,可以從配置或者註冊中心獲取。
                int coresize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_CORE);
                int maxsize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_MAX);
                int queuesize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_QUEUE);
                int keepAliveTime = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_TIME);

                BlockingQueue<Runnable> queue = ThreadPoolUtils.buildQueue(queuesize);
                NamedThreadFactory threadFactory = new NamedThreadFactory("SOFA-RPC-CB", true);

                RejectedExecutionHandler handler = new RejectedExecutionHandler() {
                    private int i = 1;

                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        if (i++ % 7 == 0) {
                            i = 1;
                            if (LOGGER.isWarnEnabled()) {
                                LOGGER.warn("Task:{} has been reject because of threadPool exhausted!" +
                                    " pool:{}, active:{}, queue:{}, taskcnt: {}", r,
                                    executor.getPoolSize(),
                                    executor.getActiveCount(),
                                    executor.getQueue().size(),
                                    executor.getTaskCount());
                            }
                        }
                        throw new RejectedExecutionException("Callback handler thread pool has bean exhausted");
                    }
                };
                asyncThreadPool = ThreadPoolUtils.newCachedThreadPool(
                    coresize, maxsize, keepAliveTime, queue, threadFactory, handler);
            }
        }
    }
    return asyncThreadPool;
}
複製程式碼

這裡也做了雙重檢查鎖。

預設核心執行緒大小 10,最大 200, 佇列大小 256, 回收時間 60 秒。

因此,獲取的佇列就是 LinkedBlockingQueue。

這裡的拒絕策略很有意思,每失敗 6 次,列印詳細資訊,當前執行緒數,活動執行緒數量,佇列 size, 任務總數,不知道為什麼這麼設計(6次??)。

目前框架中 Event 的實現很多,我們在之前的原始碼分析中也看到很多了。而訂閱者目前只有一個 FaultToleranceSubscriber。用於容錯處理。是 FaultToleranceModule 模組的功能。該功能也是個擴充套件點,當系統初始化的時候,會註冊 ClientSyncReceiveEvent 事件和 ClientAsyncReceiveEvent。

總結

這個事件匯流排功能真是觀察者模式的最佳實踐,通過系統中發生的事件,能夠讓外部模組感知到並進行處理,比如上面介紹的容錯模組。當發生訂閱的事件後,外部模組能夠響應,很完美。

相關文章