歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
《disruptor筆記》系列連結
本篇概覽
- 本文是《disruptor筆記》系列的終篇,前面我們們看了那麼多程式碼,也寫了那麼多程式碼,現在我們們去看幾個知識點,在輕鬆的閱讀過程中完成disruptor之旅;
- 要關注的知識點有以下四處:
- 偽共享
- Translators
- Lambda風格
- 清理資料
- 接下來開始逐個瞭解;
偽共享
- 下圖是多核處理器的CPU快取,可見每個核都有自己的L1和L2快取,而L3快取是共享的:
-
假設disruptor的Sequence是long型,那麼一個生產者和一個消費者的disruptor應該有兩個long型Sequence,在L1中快取這兩個數字時,因為每個快取行大小是64位元組,所以兩個Sequence很有可能在一個快取行中
-
此時如果程式修改了生產者Sequence的值,就會讓L1上對應的快取行失效,再從Main memory中讀取最新的值,此時因為消費者Sequence也在同一個快取行中,因此也被失效了,這就導致一個沒有變化的值也被清理掉,還要再去Main memory中取一次,這是影響效能的行為
-
看到這裡,聰明的您一定想到解決問題的思路:不要讓兩個Sequence在同一個快取行中
-
具體的手段呢?您有沒有聯想到日常生活中的佔座位,在身邊座位放個揹包,其他人就不能使用了(這是不文明行為,僅舉例用)
-
實際上disruptor用的也是佔座的套路,我們們來看看Sequence的原始碼就一目瞭然了,如下圖所示,Sequence的值是Value物件的成員變數value:
// 父類,
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding
{
protected volatile long value;
}
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
public class Sequence extends RhsPadding
{
...
- 類圖如下,可見Value的父子類中都是佔位的long型:
- 因此,Sequence物件有16個成員變數,在L1 cache中是下圖的排列方式:
- 想像一下,L1 cache的快取行,每64位元組為一個,也就是說上面那一串,每八個就佔據一個快取行(每個long型8位元組),於是就有以下三種排列的可能:
- V出現在一個快取行的首位;
- V出現在一個快取行的末尾;
- V出現在一個快取行的首位和末尾之間的其他六個位置之一;
- 也就是下圖三種可能(U是L1 cache中的其他內容),可見不論哪種可能,V都能用P把自己所在快取行全部佔座,這樣就不會出現兩個Sequence出現在同一個快取行的情況了:
Translators
-
Translators是個小的程式設計技巧,disruptor幫使用者做了些封裝,讓釋出事件的程式碼更簡潔;
-
開啟disruptor-tutorials專案的consume-mode這個module,回顧一下,業務釋出事件要呼叫的方法,在OrderEventProducer.java中:
public void onData(String content) {
// ringBuffer是個佇列,其next方法返回的是下最後一條記錄之後的位置,這是個可用位置
long sequence = ringBuffer.next();
try {
// sequence位置取出的事件是空事件
OrderEvent orderEvent = ringBuffer.get(sequence);
// 空事件新增業務資訊
orderEvent.setValue(content);
} finally {
// 釋出
ringBuffer.publish(sequence);
}
}
-
上面的程式碼中,其實開發者最關注的是orderEvent.setValue(content)這部分,其他幾行是我從官方demo抄的...
-
顯然disruptor也發現了這個小問題,於是從3.0版本開始提供了EventTranslatorOneArg介面,開發者將業務邏輯放入放在此介面的實現類中,至於前面程式碼中的ringBuffer.next()、ringBuffer.get(sequence)這些,以及try-finally程式碼塊這些東西統統都省去了,我們們可以將OrderEventProducer.java改造成一個新的類,程式碼如下,可見新增內部類EventTranslatorOneArg,裡面是將資料轉為事件的業務邏輯,對外提供呼叫的onData方法中,只需一行程式碼即可,和業務無關的程式碼全部省掉了:
package com.bolingcavalry.service;
import com.lmax.disruptor.EventTranslatorOneArg;
import com.lmax.disruptor.RingBuffer;
public class OrderEventProducerWithTranslator {
// 儲存資料的環形佇列
private final RingBuffer<OrderEvent> ringBuffer;
public OrderEventProducerWithTranslator(RingBuffer<OrderEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
/**
* 內部類
*/
private static final EventTranslatorOneArg<OrderEvent, String> TRANSLATOR = new EventTranslatorOneArg<OrderEvent, String>() {
@Override
public void translateTo(OrderEvent event, long sequence, String arg0) {
event.setValue(arg0);
}
};
public void onData(String content) {
ringBuffer.publishEvent(TRANSLATOR, content);
}
}
-
在consume-mode中,上述程式碼有對應的服務類TranslatorPublishServiceImpl.java,並且有對應的單元測試程式碼(ConsumeModeServiceTest.testTranslatorPublishService),這裡就不佔篇幅了,您若有興趣可以自行查閱;
-
看看ringBuffer.publishEvent的內部實現,是如何幫我們們省去之前那幾行的,首先是呼叫了sequencer.next:
@Override
public <A> void publishEvent(EventTranslatorOneArg<E, A> translator, A arg0)
{
final long sequence = sequencer.next();
translateAndPublish(translator, sequence, arg0);
}
- 再開啟translateAndPublish看看,ringBuffer.get、try-finally程式碼塊、sequencer.publish都在,這下放心了,以前我們們做的事情,現在disruptor幫我們做了,我們們可以專心業務邏輯了:
private <A> void translateAndPublish(EventTranslatorOneArg<E, A> translator, long sequence, A arg0)
{
try
{
translator.translateTo(get(sequence), sequence, arg0);
}
finally
{
sequencer.publish(sequence);
}
}
Lambda風格
- disruptor的重要API也支援Lambda表示式作為入參,這裡將幾處常用的API整理如下:
- Disruptor類例項化(LambdaServiceImpl.java):
// lambda型別的例項化
disruptor = new Disruptor<OrderEvent>(OrderEvent::new, BUFFER_SIZE, DaemonThreadFactory.INSTANCE);
- 設定事件消費者的時候,可以用Lambda取代之前的物件(LambdaServiceImpl.java):
// lambda表示式指定具體消費邏輯
disruptor.handleEventsWith((event, sequence, endOfBatch) -> {
log.info("lambda操作, sequence [{}], endOfBatch [{}], event : {}", sequence, endOfBatch, event);
// 這裡延時100ms,模擬消費事件的邏輯的耗時
Thread.sleep(100);
// 計數
eventCountPrinter.accept(null);
});
- 釋出事件的操作,也支援Lambda表示式,如下所示,我在父類ConsumeModeService.java中新增publistEvent方法,裡面呼叫的disruptor.getRingBuffer().publishEvent的入參就是Lambda表示式和事件所需的業務資料,這樣就省區了以前釋出事件的類OrderEventProducer.java:
public void publistEvent(EventTranslatorOneArg<OrderEvent, String> translator, String value) {
disruptor.getRingBuffer().publishEvent(translator, value);
}
- 如下所示,現在拿到業務資料後釋出事件的操作變得非常輕了,Lambda表示式中做好業務資料轉事件的邏輯即可,最終,不再需要OrderEventProducer.java,一行程式碼完成事件釋出(ConsumeModeServiceTest.java):
for(int i=0;i<EVENT_COUNT;i++) {
log.info("publich {}", i);
final String content = String.valueOf(i);
lambdaService.publistEvent((event, sequence, value) -> event.setValue(value), content);
}
清理資料
- 由於儲存的資料結構是環形佇列,對於每個事件的例項,會一直儲存在佇列中,直到再次在這個位置寫入時才會被新的事件例項覆蓋,考慮到可能有的場景要求資料被消費後就立即被清除,disruptor官方提供了以下建議:
- 事件定義的類中,增加一個清理業務資料的方法(假設是ObjectEvent類的clear方法);
- 新增一個事件處理類(假設是ClearingEventHandler),在裡面主動呼叫事件定義類的清理業務資料的方法;
- 在編寫事件消費邏輯時,最後新增上述事件處理類ClearingEventHandler,這樣就會呼叫ObjectEvent例項的clear方法,將業務資料清理掉;
- 官方給出的程式碼如下:
- 至此,整個《disruptor筆記》就完成了,感謝您的關注,希望這個系列的內容能給您帶來幫助,在開發中多一些選擇和參考;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos