1. Disruptor簡介
Disruptor是一個用於在線程間通信的高效低延時的消息組件,像個增強的隊列,是LMAX 公司在開發金融交易系統中的一個關鍵創新。
關於Disruptor的相關知識可以從“http://lmax-exchange.github.io/disruptor/”處獲取。Disruptor是一個輕量的框架組件,其類圖如下圖所示:
Disruptor以RingBuffer爲核心,發佈、處理註冊到其上的事件。初識Disruptor,我的感覺是很像nodejs的請求處理框架和windows的窗口過程的處理方式。調用方不斷在Disruptor上發佈事件,而Disruptor則通過預置的事件處理函數(消費者)來快速處理這些事件。幸運的是,Disruptor提供了多種靈活的消費模式,可用於解決一些常見問題。
本文中,我們希望先通過一個使用Disruptor的應用實例先熟悉Disruptor的使用,再去剖析Disruptor的架構和實現原理。
2.問題的提出
在分佈式系統中,經常會出現同一條信令發往多個節點,匯聚這多個節點的響應後,再一併發給調用方的場景。
使用Disruptor的消費者模型,如下圖所示:
在這個模型中,P1是事件發佈(生產)過程,即將要發送的信令發佈到Disruptor上,C1和C2是第一層消費者,即將信令發送到不同的節點(C1和C2代表不同節點),C3是第二層消費者,待C1和C2都完成之後才執行,即匯聚來自不同的節點的響應並返回給調用方。
3.代碼實現
示例代碼見https://github.com/solarkai/Disruptor4MultiResponseExample。
3.1 SignalDisruptorService
實現disruptor的啓動,停止,事件註冊等操作。
該服務中,定義了等待信令響應的信號量全局MAP,和信令返回響應的全局MAP,如下:
@Getter
// key爲消息的唯一標識,value爲本條信令對應的信號量
private final ConcurrentHashMap<Long, CountDownLatch> cdlMap = new ConcurrentHashMap<Long, CountDownLatch>();
@Getter
// key爲消息的唯一標識,value爲本條信令對應的迴應列表(來自多網)
private final ConcurrentHashMap<Long, List<SignalResponse>> responseMap = new ConcurrentHashMap<Long, List<SignalResponse>>();
在啓動disruptor時,根據配置的節點動態構建第一層消費者(發送信令),代碼如下:
if (!isDisruptorStarted) {
Send2NodeEventHandler[] send2NodeEventHandlerArray = new Send2NodeEventHandler[nodeNameList.size()];
for (int i = 0; i < nodeNameList.size(); i++) {
String nodeName = nodeNameList.get(i);
Send2NodeEventHandler handler = new Send2NodeEventHandler();
handler.setNodeName(nodeName);
send2NodeEventHandlerArray[i] = handler;
}
// 設置消費者依賴圖
disruptor.handleEventsWith(send2NodeEventHandlerArray).then(rceh);
disruptor.start();
isDisruptorStarted = true;
}
3.2 事件處理定義
信令的請求和響應通過信令的唯一標示ID字段關聯。第一層消費者使用Send2NodeEventHandler定義,只完成發送信令到節點的處理,第二層消費者使用ResponseCollectionEventHandler定義,完成信令在各節點響應的匯聚工作。
在disputor上發佈一條信令事件的代碼如下,發佈一條信令後,同時也初始化了該條信令對應的信號量(CountDownLatch):
RingBuffer<SignalSendEvent> rb = disruptor.getRingBuffer();
long sequence = rb.next();
try {
SignalSendEvent event = rb.get(sequence);
event.setDsm(dsm);
// 放入回調函數可訪問的map
this.cdlMap.put(dsm.getId(), new CountDownLatch(nodeNameList.size()));
this.responseMap.put(dsm.getId(), new ArrayList<SignalResponse>());
} finally {
rb.publish(sequence);
}
return sequence;
ResponseCollectionEventHandler在此信令對應的信號量上等待,匯聚所有響應,代碼如下:
long id = event.getDsm().getId();
// 阻塞等待所有節點對信令的響應
CountDownLatch latch = sds.getCdlMap().get(id);
if (null != latch) {
log.info("start to wait response by id:{}",id);
latch.await(SIGNAL_TIMEOUT, TimeUnit.MILLISECONDS);
// 阻塞結束,處理所有響應
List<SignalResponse> respList = sds.getResponseMap().get(id);
handleRespList(respList);
// 清除資源
sds.getCdlMap().remove(event.getDsm().getId());
sds.getResponseMap().remove(event.getDsm().getId());
} else {
log.error("event not set latch:{}", event);
}
3.3 模擬信令響應
使用SignalDisruptorController中的函數responseSignal模擬節點對信令的響應,代碼如下:
@RequestMapping(value = "/responsesignal", method = RequestMethod.POST)
@ApiOperation(value = "siganl響應", notes = "siganl響應")
public long responseSignal(@RequestBody(required = true) SignalResponse sr) {
long id = sr.getId();
CountDownLatch cdl = sds.getCdlMap().get(id);
if (null != cdl) {
cdl.countDown();
}
List<SignalResponse> respList = sds.getResponseMap().get(id);
if (null != respList) {
respList.add(sr);
}
return id;
}
3.4 執行結果
執行該代碼,發現在模擬一條信令發佈後,disruptor執行第一層消費者,使用3個線程(代碼中配了3個節點)分別模擬發送信令過程,第二層消費者(響應匯聚)在第一層消費者都執行完之後再執行,並分配到一個新的線程。
從代碼執行打印,可以看到同一個EventHandler一直在同一個線程上執行,因而在編程中需關注某個事件的EventHandler處理時間過長會阻塞後一個事件的處理。
4 小結
通過disruptor的消費依賴圖定義,的確簡化了多請求響應的代碼處理。後面我們會再根據執行結果分析一下Disruptor的實現原理。