【開發總結】Disruptor 使用簡介
在極客時間看到王寶令老師關於 Disruptor 的一篇文章,覺得很有意思。看完之後又在網上找到一些其他關於Disruptor 的資料看了一下。
現在寫篇文章總結一下。
使用
Disruptor 百度翻譯是干擾者,分裂器的意思。
在這裡它其實是一個高效能佇列,一個queue。所以我有點想不通為什麼名字取成這樣。有清楚的同學可以知會我一生。
Disruptor 的使用相對Java集合類中的佇列,會更加複雜。
第一步,引入jar包.
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
第二步,生成 Disruptor 物件
第三步,設定佇列中訊息消費的handler.
第四步,啟動 Disruptor 執行緒。
第五步,獲取ringbuffer。生產者通過向 Disruptor 的ringbuffer 來發布訊息的。所以事先要先獲取ringbuffer。
第六步,釋出訊息。
/**
* @description:
* @author: lkb
* @create: 2020-10-28 19:46
*/
@Slf4j
public class MyTest {
public static void main(String[] args) {
//指定RingBuffer大小,
//必須是2的N次方
int bufferSize = 1024;
//構建Disruptor
Disruptor<LongEvent> disruptor
= new Disruptor<>(
LongEvent::new,
bufferSize,
DaemonThreadFactory.INSTANCE);
//註冊事件處理器
disruptor.handleEventsWith(
(event, sequence, endOfBatch) ->
System.out.println("E: " + event));
//啟動Disruptor
disruptor.start();
//獲取RingBuffer
RingBuffer<LongEvent> ringBuffer
= disruptor.getRingBuffer();
//生產Event
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++) {
bb.putLong(0, l);
//生產者生產訊息
ringBuffer.publishEvent(
(event, sequence, buffer) ->
event.set(buffer.getLong(0)), bb);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
}
}
其中LongEvent 是一個普通的POJO物件
public class LongEvent {
private long value;
public void set(long value) {
this.value = value;
}
}
Disruptor 的使用是典型的生產者-消費者模式。
Java集合中的佇列更符合我們對佇列的操作習慣。以ArrayBlockingQueue為例,我們可以把ArrayBlockingQueue想象為一個佇列管道,生產者執行緒生產完資料後,將資料丟到佇列中,消費者執行緒從另外一端取出資料,進行消費。
而Disruptor 相對其他傳統的佇列而言更像一個“大家長”,生成者需要通過這位“大家長”的ringbuffer將訊息傳送出去,消費者需要將處理操作註冊到“大家長”這裡。
這樣的佇列操作不太符合我們的習慣,所以使用上會不那麼順手。
高效的祕訣
在不順手的情況下,為什麼還是有很多系統用到它呢?原因在於它非常高效。
上面是網上找到的效能對比圖。可以看到Disruptor效能上是非常高的。
那它是如何實現高效的呢?
- 記憶體分配更加合理,使用 RingBuffer 資料結構,陣列元素在初始化時一次性全部建立,提升快取命中率;
- 物件迴圈利用,避免頻繁 GC。
- 能夠避免偽共享,提升快取利用率。
- 採用無鎖演算法,避免頻繁加鎖、解鎖的效能消耗。支援批量消費,消費者可以無鎖方式消費多個訊息。
對於第四點,相信大家都很清楚。鎖操作涉及到作業系統狀態切換,這個操作是非常耗時耗資源的。無鎖操作可以避免狀態切換。
對於前面三點,涉及到一個非常重要的概念,就是快取。CPU有三級快取。離CPU越近的快取,速度越快,但是容量越小。因為CPU的速度遠遠大於其他硬體的速度,設定快取能夠減小CPU和其他硬體的速度差。這個快取和生產者消費者中間的佇列有異曲同工之妙。
為了提高快取的命中率,硬體通過區域性性原理,在載入一個資料的同時將它周圍的資料也載入進去。
程式的區域性性原理指的是在一段時間內程式的執行會限定在一個區域性範圍內。這裡的“區域性性”可以從兩個方面來理解,一個是時間區域性性,另一個是空間區域性性。時間區域性性指的是程式中的某條指令一旦被執行,不久之後這條指令很可能再次被執行;如果某條資料被訪問,不久之後這條資料很可能再次被訪問。而空間區域性性是指某塊記憶體一旦被訪問,不久之後這塊記憶體附近的記憶體也很可能被訪問。
上訴的第1、2條,通過將資料設定進連續相鄰的記憶體位置,CPU在讀取了一個資料的時候,發現第二個資料已經因為“區域性性”原理載入進快取,就不需要再次去定址,直接從快取中獲取資料。
第1,2條是對快取的高效利用,第3條就是對快取低效使用的規避。
有一種快取低效使用的方式是“偽共享”。記憶體是按照快取行進行管理的。快取行的大小通常是64個位元組。
例如一個快取行儲存了兩個物件,對其中一個物件的操作會使得整個快取行失效。也就是說即使物件B被加入了快取,但是因為其他物件的操作無效了。
第3條中,Disruptor 中通過將物件包裹,讓一個物件充滿整個快取行,避免了偽共享的問題。
還有一點就是,相對於其他阻塞佇列,Disruptor 的等待策略更多,功能更加強大。
通過對快取的利用和無鎖操作,Disruptor 成為一個高效佇列。
一些思考
Disruptor 的一些思想其實在其他框架上也是常見的。
避免偽共享問題上,MySQL 8.0 版本直接將查詢快取的整塊功能刪掉了;在高效利用快取上,執行緒池、佇列等都多算快取概念的受益者;避免鎖操作上,Java的底層的各種鎖優化,也是利用這點,比如輕量級鎖。
為什麼這麼多框架會不約而同地想到這些問題呢?
因為計算機、作業系統是非常成熟的,底層都是非常相似的架構。瞭解計算機底層原理,對這些知識才能觸類旁通。所以,啥不說,計算機基礎課,我打算再上一遍。