前言
大部分框架都是事件訂閱功能,即觀察者模式,或者叫事件機制。通過訂閱某個事件,當觸發事件時,回撥某個方法。該功能非常的好用,而 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。
總結
這個事件匯流排功能真是觀察者模式的最佳實踐,通過系統中發生的事件,能夠讓外部模組感知到並進行處理,比如上面介紹的容錯模組。當發生訂閱的事件後,外部模組能夠響應,很完美。