disruptor筆記之八:知識點補充(終篇)

程式設計師欣宸發表於2021-10-08

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

《disruptor筆記》系列連結

  1. 快速入門
  2. Disruptor類分析
  3. 環形佇列的基礎操作(不用Disruptor類)
  4. 事件消費知識點小結
  5. 事件消費實戰
  6. 常見場景
  7. 等待策略
  8. 知識點補充(終篇)

本篇概覽

  • 本文是《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的值是<font color="blue">Value</font>物件的成員變數<font color="red">value</font>:
// 父類,
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位元組),於是就有以下三種排列的可能:
  1. V出現在一個快取行的首位;
  2. V出現在一個快取行的末尾;
  3. 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);
        }
    }
  • 上面的程式碼中,其實開發者最關注的是<font color="blue">orderEvent.setValue(content)</font>這部分,其他幾行是我從官方demo抄的...
  • 顯然disruptor也發現了這個小問題,於是從3.0版本開始提供了<font color="blue">EventTranslatorOneArg</font>介面,開發者將業務邏輯放入放在此介面的實現類中,至於前面程式碼中的<font color="blue">ringBuffer.next()</font>、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的內部實現,是如何幫我們們省去之前那幾行的,首先是呼叫了<font color="blue">sequencer.next</font>:
@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整理如下:
  1. Disruptor類例項化(LambdaServiceImpl.java):
// lambda型別的例項化
disruptor = new Disruptor<OrderEvent>(OrderEvent::new, BUFFER_SIZE, DaemonThreadFactory.INSTANCE);
  1. 設定事件消費者的時候,可以用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);
        });
  1. 釋出事件的操作,也支援Lambda表示式,如下所示,我在父類ConsumeModeService.java中新增publistEvent方法,裡面呼叫的disruptor.getRingBuffer().publishEvent的入參就是Lambda表示式和事件所需的業務資料,這樣就省區了以前釋出事件的類OrderEventProducer.java:
public void publistEvent(EventTranslatorOneArg<OrderEvent, String> translator, String value) {
        disruptor.getRingBuffer().publishEvent(translator, value);
    }
  1. 如下所示,現在拿到業務資料後釋出事件的操作變得非常輕了,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官方提供了以下建議:
  1. 事件定義的類中,增加一個清理業務資料的方法(假設是ObjectEvent類的clear方法);
  2. 新增一個事件處理類(假設是ClearingEventHandler),在裡面主動呼叫事件定義類的清理業務資料的方法;
  3. 在編寫事件消費邏輯時,最後新增上述事件處理類ClearingEventHandler,這樣就會呼叫ObjectEvent例項的clear方法,將業務資料清理掉;
  • 官方給出的程式碼如下:

在這裡插入圖片描述

  • 至此,整個《disruptor筆記》就完成了,感謝您的關注,希望這個系列的內容能給您帶來幫助,在開發中多一些選擇和參考;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章