Disruptor__快

Disruptor提供了一種線程之間信息交換的方式。

鎖的缺點

并發(fā)的問題


想象有兩個線程嘗試修改同一個變量value:

  • 情況一:線程1先到達

變量value的值變?yōu)椤眀lah”。
然后當線程2到達時,變量value的值變?yōu)椤眀lahy”。

  • 情況二:線程2先到達

變量value的值變?yōu)椤眆luffy”。
然后當線程1到達時,值變?yōu)椤眀lah”。

  • 情況三:線程1與線程2交互

線程2得到值"fluff"然后賦給本地變量myValue。
線程1改變value的值為”blah”。
然后線程2醒來并把變量value的值改為”fluffy”

解決辦法

悲觀鎖

樂觀鎖(CAS)


在這種情況,當線程2需要去寫Entry時才會去鎖定它.它需要檢查Entry自從上次讀過后是否已經被改過了。如果線程1在線程2讀完后到達并把值改為”blah”,線程2讀到了這個新值,線程2不會把"fluffy"寫到Entry里并把線程1所寫的數據覆蓋.線程2會重試(重新讀新的值,與舊值比較,如果相等則在變量的值后面附上’y’),這里在線程2不會關心新的值是什么的情況.或者線程2會拋出一個異常,或者會返回一個某些字段已更新的標志,這是在期望把”fluff”改為”fluffy”的情況.舉一個第二種情況的例子,如果你和另外一個用戶同時更新一個Wiki的頁面,你會告訴另外一個用戶的線程 Thread 2,它們需要重新加載從Thread1來新的變化,然后再提交它們的內容。

死鎖

死鎖的原因和解決方法?

鎖技術是慢的

Disruptor論文中講述了我們所做的一個實驗。這個測試程序調用了一個函數,該函數會對一個64位的計數器循環(huán)自增5億次。當單線程無鎖時,程序耗時300ms。如果增加一個鎖(仍是單線程、沒有競爭、僅僅增加鎖),程序需要耗時10000ms,慢了兩個數量級。更令人吃驚的是,如果增加一個線程(簡單從邏輯上想,應該比單線程加鎖快一倍),耗時224000ms。使用兩個線程對計數器自增5億次比使用無鎖單線程慢1000倍。并發(fā)很難而鎖的性能糟糕。

Disruptor如何解決這些問題。

  • 首先,Disruptor根本就不用鎖。
    取而代之的是,在需要確保操作是線程安全的(特別是,在多生產者的環(huán)境下,更新下一個可用的序列號)地方,我們使用CAS(Compare And Swap/Set)操作。這是一個CPU級別的指令,在我的意識中,它的工作方式有點像樂觀鎖——CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。

CAS操作比鎖消耗資源少的多,因為它們不牽涉操作系統,它們直接在CPU上操作。但它們并非沒有代價——在上面的試驗中,單線程無鎖耗時300ms,單線程有鎖耗時10000ms,單線程使用CAS耗時5700ms。所以它比使用鎖耗時少,但比不需要考慮競爭的單線程耗時多。

回到Disruptor,在我講生產者時講過ClaimStrategy。在這些代碼中,你可以看見兩個策略,一個是SingleThreadedStrategy(單線程策略)另一個是MultiThreadedStrategy(多線程策略)。你可能會有疑問,為什么在只有單個生產者時不用多線程的那個策略?它是否能夠處理這種場景?當然可以。但多線程的那個使用了AtomicLong(Java提供的CAS操作),而單線程的使用long,沒有鎖也沒有CAS。這意味著單線程版本會非常快,因為它只有一個生產者,不會產生序號上的沖突。

  • 當然把一個數字轉成AtomicLong不可能是Disruptor速度快的唯一秘密
    但這是非常重要的一點——在整個復雜的框架中,只有這一個地方出現多線程競爭修改同一個變量值。這就是秘密。還記得所有的訪問對象都擁有序號嗎?如果只有一個生產者,那么系統中的每一個序列號只會由一個線程寫入。這意味著沒有競爭、不需要鎖、甚至不需要CAS。

為什么隊列不能勝任這個工作

為什么隊列底層用RingBuffer來實現,仍然在性能上無法與 Disruptor 相比。隊列和最簡單的ring buffer只有兩個指針——一個指向隊列的頭,一個指向隊尾:


如果有超過一個生產者想要往隊列里放東西,尾指針就將成為一個沖突點,因為有多個線程要更新它。如果有多個消費者,那么頭指針就會產生競爭,因為元素被消費之后,需要更新指針,所以不僅有讀操作還有寫操作了。
一個空的隊列:


一個滿的隊列:


隊列需要保存一個關于大小的變量,以便區(qū)分隊列是空還是滿。否則,它需要根據隊列中的元素的內容來判斷。
基于以上,這三個變量常常在一個cache line里面,有可能導致false sharing。因此,不僅要擔心生產者和消費者同時寫size變量(或者元素),還要注意由于頭指針尾指針在同一位置,當頭指針更新時,更新尾指針會導致緩存不命中。

神奇的緩存行填充

緩存行,偽共享

神奇的緩存行填充

Disruptor消除這個問題,至少對于緩存行大小是64字節(jié)或更少的處理器架構來說是這樣的(譯注:有可能處理器的緩存行是128字節(jié),那么使用64字節(jié)填充還是會存在偽共享問題),通過增加補全來確保ring buffer的序列號不會和其他東西同時存在于一個緩存行中。

public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding

因此沒有偽共享,就沒有和其它任何變量的意外沖突,沒有不必要的緩存未命中。

內存屏障

  • 什么是內存屏障?
    它是一個CPU指令。
    a)確保一些特定操作執(zhí)行的順序;
    b)影響一些數據的可見性(可能是某些指令執(zhí)行后的結果)。

如果你的字段是volatile,Java內存模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。
1、一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。

2、在你寫入前,會保證所有之前發(fā)生的事已經發(fā)生,并且任何更新過的數據值也是可見的,因為內存屏障會把之前的寫入值都刷新到緩存。

RingBuffer cursor

RingBuffer的指針(cursor)(譯注:指向隊尾元素)屬于一個神奇的volatile變量,同時也是我們能夠不用鎖操作就能實現Disruptor的原因之一。

生產者將會取得下一個[Entry](或者是一批),并可對它(們)作任意改動, 把它(們)更新為任何想要的值。在所有改動都完成后,生產者對ring buffer調用commit方法來更新序列號(譯注:把cursor更新為該Entry的序列號)。對volatile字段(cursor)的寫操作創(chuàng)建了一個內存屏障,這個屏障將刷新所有緩存里的值(或者至少相應地使得緩存失效)。
這時候,消費者們能獲得最新的序列號碼(8),并且因為內存屏障保證了它之前執(zhí)行的指令的順序,消費者們可以確信生產者對7號Entry所作的改動已經可用。

  • 消費者那邊會發(fā)生什么?
    消費者中的序列號是volatile類型的,會被若干個外部對象讀取——其他的[下游消費者]可能在跟蹤這個消費者。[ProducerBarrier]/RingBuffer跟蹤它以確保環(huán)沒有出現重疊(wrap)的情況(譯注:為了防止下游的消費者和上游的消費者對同一個Entry競爭消費,導致在環(huán)形隊列中互相覆蓋數據,下游消費者要對上游消費者的消費情況進行跟蹤)。

所以,如果你的下游消費者(C2)看見前一個消費者(C1)在消費號碼為12的Entry,當C2的讀取也到了12,它在更新序列號前將可以獲得C1對該Entry的所作的更新。

基本來說就是,C1更新序列號前對ring buffer的所有操作(如上圖黑色所示),必須先發(fā)生,待C2拿到C1更新過的序列號之后,C2才可以為所欲為(如上圖藍色所示)。

  • 內存屏障對性能的影響
    內存屏障作為另一個CPU級的指令,沒有[鎖那樣大的開銷]。內核并沒有在多個線程間干涉和調度。但凡事都是有代價的。內存屏障的確是有開銷的——編譯器/cpu不能重排序指令,導致不可以盡可能地高效利用CPU,另外刷新緩存亦會有開銷。所以不要以為用volatile代替鎖操作就一點事都沒。
    你會注意到Disruptor的實現對序列號的讀寫頻率盡量降到最低。對volatile字段的每次讀或寫都是相對高成本的操作。但是,也應該認識到在批量的情況下可以獲得很好的表現。如果你知道不應對序列號頻繁讀寫,那么很合理的想到,先獲得一整批Entries,并在更新序列號前處理它們。這個技巧對生產者和消費者都適用。以下的例子來自[BatchConsumer]
    long nextSequence = sequence + 1;
    while (running)
    {
        try
        {
            final long availableSequence = consumerBarrier.waitFor(nextSequence);
            while (nextSequence <= availableSequence)
            {
                entry = consumerBarrier.getEntry(nextSequence);
                handler.onAvailable(entry);
                nextSequence++;
            }
            handler.onEndOfBatch();
            sequence = entry.getSequence();
        }
        …
        catch (final Exception ex)
        {
            exceptionHandler.handle(ex, entry);
            sequence = entry.getSequence();
            nextSequence = entry.getSequence() + 1;
        }
    }

在上面的代碼中,我們在消費者處理entries的循環(huán)中用一個局部變量(nextSequence)來遞增。這表明我們想盡可能地減少對volatile類型的序列號的進行讀寫。

牛逼的RingBuffer

基本來說,ringbuffer擁有一個序號,這個序號指向數組中下一個可用的元素。(校對注:如下圖右邊的圖片表示序號,這個序號指向數組的索引4的位置。
隨著你不停地填充這個buffer(可能也會有相應的讀取),這個序號會一直增長,直到繞過這個環(huán)。
要找到數組中當前序號指向的元素,可以通過mod操作:
以上面的ringbuffer為例(java的mod語法):12 % 10 = 2。很簡單吧。

事實上,上圖中的ringbuffer只有10個槽完全是個意外。如果槽的個數是2的N次方更有利于基于二進制的計算機進行計算。

(校對注:2的N次方換成二進制就是1000,100,10,1這樣的數字, sequence & (array length-1) = array index,比如一共有8槽,3&(8-1)=3,HashMap就是用這個方式來定位數組元素的,這種方式比取模的速度更快。)

  • 沒有尾指針。只維護了一個指向下一個可用位置的序號。這種實現是經過深思熟慮的—我們選擇用環(huán)形buffer的最初原因就是想要竟可能多地保存消息(若有了尾指針,則尾指針經過的空間又浪費了)。
  • 不刪除buffer中的數據
    也就是說這些數據一直存放在buffer中,直到新的數據覆蓋他們。這也是不需要尾指針的原因。
  • 它是數組
    這是對CPU緩存友好的—也就是說,在硬件級別,數組中的元素是會被預加載的,因此在ringbuffer當中,cpu無需時不時去主存加載數組中的下一個元素。
  • 為數組預先分配內存
    這使得數組對象一直存在(除非程序終止)。這就意味著不需要花大量的時間用于垃圾回收。

從Ringbuffer中讀取數據消費


消費者(Consumer)是一個想從Ring Buffer里讀取數據的線程,它可以訪問ConsumerBarrier對象——這個對象由RingBuffer創(chuàng)建并且代表消費者與RingBuffer進行交互。就像Ring Buffer顯然需要一個序號才能找到下一個可用節(jié)點一樣,消費者也需要知道它將要處理的序號——每個消費者都需要找到下一個它要訪問的序號。在上面的例子中,消費者處理完了Ring Buffer里序號8之前(包括8)的所有數據,那么它期待訪問的下一個序號是9。

消費者可以調用ConsumerBarrier對象的waitFor()方法,傳遞它所需要的下一個序號.
final long availableSeq = consumerBarrier.waitFor(nextSequence);
ConsumerBarrier返回RingBuffer的最大可訪問序號——在上面的例子中是12。ConsumerBarrier有一個WaitStrategy方法來決定它如何等待這個序號。

接下來

接下來,消費者會一直原地停留,等待更多數據被寫入Ring Buffer。并且,一旦數據寫入后消費者會收到通知——節(jié)點9,10,11和12 已寫入。現在序號12到了,消費者可以讓ConsumerBarrier去拿這些序號節(jié)點里的數據了。


拿到了數據后,消費者(Consumer)會更新自己的標識(cursor)。

你應該已經感覺得到,這樣做是怎樣有助于平緩延遲的峰值了——以前需要逐個節(jié)點地詢問“我可以拿下一個數據嗎?現在可以了么?現在呢?”,消費者(Consumer)現在只需要簡單的說“當你拿到的數字比我這個要大的時候請告訴我”,函數返回值會告訴它有多少個新的節(jié)點可以讀取數據了。因為這些新的節(jié)點的確已經寫入了數據(Ring Buffer本身的序號已經更新),而且消費者對這些節(jié)點的唯一操作是讀而不是寫,因此訪問不用加鎖。這太好了,不僅代碼實現起來可以更加安全和簡單,而且不用加鎖使得速度更快。

另一個好處是——你可以用多個消費者(Consumer)去讀同一個RingBuffer ,不需要加鎖,也不需要用另外的隊列來協調不同的線程(消費者)。這樣你可以在Disruptor的協調下實現真正的并發(fā)數據處理。

寫入 Ringbuffer

ProducerBarriers(AbstractSequencer)

寫入 Ring Buffer 的過程涉及到兩階段提交 (two-phase commit)。首先,你的生產者需要申請 buffer 里的下一個節(jié)點。然后,當生產者向節(jié)點寫完數據,它將會調用 ProducerBarrier 的 commit 方法。

那么讓我們首先來看看第一步。 “給我 Ring Buffer 里的下一個節(jié)點”,這句話聽起來很簡單。的確,從生產者角度來看它很簡單:簡單地調用 ProducerBarrier 的 nextEntry() 方法,這樣會返回給你一個 Entry 對象,這個對象就是 Ring Buffer 的下一個節(jié)點。

如何防止 Ring Buffer 重疊

在后臺,由 ProducerBarrier 負責所有的交互細節(jié)來從 Ring Buffer 中找到下一個節(jié)點,然后才允許生產者向它寫入數據。


在這幅圖中,我們假設只有一個生產者寫入 Ring Buffer。過一會兒我們再處理多個生產者的復雜問題。

ConsumerTrackingProducerBarrier 對象擁有所有正在訪問 Ring Buffer 的 消費者 列表。這看起來有點兒奇怪-我從沒有期望 ProducerBarrier 了解任何有關消費端那邊的事情。但是等等,這是有原因的。因為我們不想與隊列“混為一談”(隊列需要追蹤隊列的頭和尾,它們有時候會指向相同的位置),Disruptor 由消費者負責通知它們處理到了哪個序列號,而不是 Ring Buffer。所以,如果我們想確定我們沒有讓 Ring Buffer 重疊,需要檢查所有的消費者們都讀到了哪里。
在上圖中,有一個 消費者 順利的讀到了最大序號 12(用紅色/粉色高亮)。第二個消費者 有點兒落后——可能它在做 I/O 操作之類的——它停在序號 3。因此消費者 2 在趕上消費者 1 之前要跑完整個 Ring Buffer 一圈的距離。

現在生產者想要寫入 Ring Buffer 中序號 3 占據的節(jié)點,因為它是 Ring Buffer 當前游標的下一個節(jié)點。但是 ProducerBarrier 明白現在不能寫入,因為有一個消費者正在占用它。所以,ProducerBarrier 停下來自旋 (spins),等待,直到那個消費者離開。

申請下一個節(jié)點

現在可以想像消費者 2 已經處理完了一批節(jié)點,并且向前移動了它的序號。可能它挪到了序號 9(因為消費端的批處理方式,現實中我會預計它到達 12,但那樣的話這個例子就不夠有趣了)。

上圖顯示了當消費者 2 挪動到序號 9 時發(fā)生的情況。在這張圖中我已經忽略了ConsumerBarrier,因為它沒有參與這個場景。
ProducerBarier 會看到下一個節(jié)點——序號 3 那個已經可以用了。它會搶占這個節(jié)點上的 Entry(我還沒有特別介紹 Entry 對象,基本上它是一個放寫入到某個序號的 Ring Buffer 數據的桶),把下一個序號(13)更新成 Entry 的序號,然后把 Entry 返回給生產者。生產者可以接著往 Entry 里寫入數據。

提交新的數據


當生產者結束向 Entry 寫入數據后,它會要求 ProducerBarrier 提交。

ProducerBarrier 先等待 Ring Buffer 的游標追上當前的位置(對于單生產者這毫無意義-比如,我們已經知道游標到了 12 ,而且沒有其他人正在寫入 Ring Buffer)。然后 ProducerBarrier 更新 Ring Buffer 的游標到剛才寫入的 Entry 序號-在我們這兒是 13。接下來,ProducerBarrier 會讓消費者知道 buffer 中有新東西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 對象說-“喂,醒醒!有事情發(fā)生了!”

ProducerBarrier 上的批處理

有趣的是 Disruptor 可以同時在生產者和 [消費者]兩端實現批處理。還記得伴隨著程序運行,消費者 2 最后達到了序號 9 嗎?ProducerBarrier 可以在這里做一件很狡猾的事-它知道 Ring Buffer 的大小,也知道最慢的消費者位置。因此它能夠發(fā)現當前有哪些節(jié)點是可用的。


如果 ProducerBarrier 知道 Ring Buffer 的游標指向 12,而最慢的消費者在 9 的位置,它就可以讓生產者寫入節(jié)點 3,4,5,6,7 和 8,中間不需要再次檢查消費者的位置。

多個生產者的場景

在多個生產者的場景下,你還需要其他東西來追蹤序號。這個序號是指當前可寫入的序號。注意這和“向 Ring Buffer 的游標加 1”不一樣-如果你有一個以上的生產者同時在向 Ring Buffer 寫入,就有可能出現某些 Entry 正在被生產者寫入但還沒有提交的情況。


讓我們復習一下如何申請寫入節(jié)點。每個生產者都向 ClaimStrategy 申請下一個可用的節(jié)點。生產者 1 拿到序號 13,這和上面單個生產者的情況一樣。生產者 2 拿到序號 14,盡管 Ring Buffer的當前游標僅僅指向 12。這是因為 ClaimSequence 不但負責分發(fā)序號,而且負責跟蹤哪些序號已經被分配。
我把生產者 1 和它的寫入節(jié)點涂上綠色,把生產者 2 和它的寫入節(jié)點涂上可疑的粉色-看起來像紫色。

現在假設生產者 1 還生活在童話里,因為某些原因沒有來得及提交數據。生產者 2 已經準備好提交了,并且向 ProducerBarrier 發(fā)出了請求。

就像我們先前在 commit 示意圖中看到的一樣,ProducerBarrier 只有在 Ring Buffer 游標到達準備提交的節(jié)點的前一個節(jié)點時它才會提交。在當前情況下,游標必須先到達序號 13 我們才能提交節(jié)點 14 的數據。但是我們不能這樣做,因為生產者 1 正盯著一些閃閃發(fā)光的東西,還沒來得及提交。因此 ClaimStrategy 就停在那兒自旋 (spins), 直到 Ring Buffer 游標到達它應該在的位置。


現在生產者 1 從迷糊中清醒過來并且申請?zhí)峤还?jié)點 13 的數據(生產者 1 發(fā)出的綠色箭頭代表這個請求)。ProducerBarrier 讓 ClaimStrategy 先等待 Ring Buffer 的游標到達序號 12,當然現在已經到了。因此 Ring Buffer 移動游標到 13,讓 ProducerBarrier 戳一下 WaitStrategy 告訴所有人都知道 Ring Buffer 有更新了。現在 ProducerBarrier 可以完成生產者 2 的請求,讓 Ring Buffer 移動游標到 14,并且通知所有人都知道。

你會看到,盡管生產者在不同的時間完成數據寫入,但是 Ring Buffer 的內容順序總是會遵循 nextEntry() 的初始調用順序。也就是說,如果一個生產者在寫入 Ring Buffer 的時候暫停了,只有當它解除暫停后,其他等待中的提交才會立即執(zhí)行。

更新:最近的 RingBuffer? 版本去掉了 Producer Barrier。如果在你看的代碼里找不到 ProducerBarrier,那就假設當我講“Producer Barrier”時,我的意思是“Ring Buffer”。
更新2:注意 Disruptor 2.0 版使用了與本文不一樣的命名。如果你對類名感到困惑,請閱讀我寫的Disruptor 2.0更新摘要?。

Disruptor的依賴關系

菱形結構

  • 用隊列實現菱形結構
  • 用 Disruptor 實現菱形結構


生產者這邊比較簡單,它是我在 上文? 中描述過的單生產者模型。有趣的是,生產者并不需要關心所有的消費者。它只關心消費者 C3,如果消費者 C3 處理完了 Ring Buffer 的某一個節(jié)點,那么另外兩個消費者肯定也處理完了。因此,只要 C3 的位置向前移動,Ring Buffer 的后續(xù)節(jié)點就會空閑出來。
管理消費者的依賴關系需要兩個 ConsumerBarrier 對象。第一個僅僅與 Ring Buffer 交互,C1 和 C2 消費者向它申請下一個可訪問節(jié)點。第二個 ConsumerBarrier 只知道消費者 C1 和 C2,它返回兩個消費者訪問過的消息序號中較小的那個。

Disruptor 怎樣實現消費者等待(依賴)


我們從這個故事發(fā)生到一半的時候來看:生產者 P1 已經在 Ring Buffer 里寫到序號 22 了,消費者 C1 已經訪問和處理完了序號 21 之前的所有數據。消費者 C2 處理到了序號 18。消費者 C3,就是依賴其他消費者的那個,才處理到序號 15。

生產者 P1 不能繼續(xù)向 RingBuffer 寫入數據了,因為序號 15 占據了我們想要寫入序號 23 的數據節(jié)點 (Slot)。


第一個 ConsumerBarrier(CB1)告訴 C1 和 C2 消費者可以去訪問序號 22 前面的所有數據,這是 Ring Buffer 中的最大序號。第二個 ConsumerBarrier (CB2) 不但會檢查 RingBuffer 的序號,也會檢查另外兩個消費者的序號并且返回它們之間的最小值。因此,三號消費者被告知可以訪問 Ring Buffer 里序號 18 前面的數據。

注意這些消費者還是直接從 Ring Buffer 拿數據節(jié)點——并不是由 C1 和 C2 消費者把數據節(jié)點從 Ring Buffer 里取出再傳遞給 C3 消費者的。作為替代的是,由第二個 ConsumerBarrier 告訴 C3 消費者,在 RingBuffer 里的哪些節(jié)點可以安全的處理。

這產生了一個問題——如果任何數據都來自于 Ring Buffer,那么 C3 消費者如何讀到前面兩個消費者處理完成的數據呢?如果 C3 消費者關心的只是先前的消費者是否已經完成它們的工作(例如,把數據復制到別的地方),那么這一切都沒有問題—— C3 消費者知道工作已完成就放心了。但是,如果 C3 消費者需要訪問先前的消費者的處理結果,它又從哪里去獲取呢?

秘密在于把處理結果寫入 Ring Buffer 數據節(jié)點 (Entry) 本身。這樣,當 C3 消費者從 Ring Buffer 取出節(jié)點時,它已經填充好了 C3 消費者工作需要的所有信息。這里 真正 重要的地方是節(jié)點 (Entry) 對象的每一個字段應該只允許一個消費者寫入。這可以避免產生并發(fā)寫入沖突 (write-contention) 減慢了整個處理過程。

  • 一些實際的 Java 代碼
ConsumerBarrier consumerBarrier1 =
    ringBuffer.createConsumerBarrier();
BatchConsumer consumer1 =
    new BatchConsumer(consumerBarrier1, handler1);
BatchConsumer consumer2 =
    new BatchConsumer(consumerBarrier1, handler2);
ConsumerBarrier consumerBarrier2 =
    ringBuffer.createConsumerBarrier(consumer1, consumer2);
BatchConsumer consumer3 =
    new BatchConsumer(consumerBarrier2, handler3);
ProducerBarrier producerBarrier =
    ringBuffer.createProducerBarrier(consumer3);
  • 使用DSL
Executor executor = Executors.newCachedThreadPool();
BatchHandler handler1 = new MyBatchHandler1();
BatchHandler handler2 = new MyBatchHandler2();
BatchHandler handler3 = new MyBatchHandler3();
DisruptorWizard dw = new DisruptorWizard(ENTRY_FACTORY,
    RING_BUFFER_SIZE, executor);
dw.consumeWith(handler1, handler2).then(handler3);
ProducerBarrier producerBarrier = dw.createProducerBarrier();
dw.consumeWith(handler1a, handler2a);
dw.after(handler1a).consumeWith(handler1b);
dw.after(handler2a).consumeWith(handler2b);
dw.after(handler1b, handler2b).consumeWith(handler3);
ProducerBarrier producerBarrier = dw.createProducerBarrier();

現在你知道了——如何關聯 Disruptor 與相互依賴(等待)的多個消費者。關鍵點是:

  • 使用多個 ConsumerBarrier 來管理消費者之間的依賴(等待)關系。
  • 使用 ProducerBarrier 監(jiān)視結構圖中最后一個消費者。
  • 只允許一個消費者更新數據節(jié)點 (Entry) 的每一個獨立字段。

Disruptor 2.0更新摘要

對于2.0版的主要變化有3點:

  • 更貼切的命名;
  • 把producer barrier(生產者屏障)整合進了ring buffer;
  • 將Disruptor Wizard加入了主代碼庫。
    新版本:

你可以看到基本原理還是類似的。新版本更加簡單,因為ProducerBarrier本身不再作為一個單獨的實體存在,它的替代者是PublishPort 接口,且RingBuffer自身就實現了這個接口。 類似地,DependencyBarrier替代ConsumerBarrier ,厘清了此對象的職責。另外,Publisher (Producer的替代者)和EventProcessor(替代了Consumer)也更能精確地體現出它們的行為。Consumer這個名字總是會帶來一些混淆,因為因為其實消費者從來不從ring buffer消費任何東西。Consumer之前僅僅是用于隊列實現的一個術語。

圖上沒有表現出來的變動是以前存在ring buffer里的東西叫entry(輸入條目),而現在改名叫Event(事件)了,相應的就是EventProcessor。

概念

RingBuffer: 被看作Disruptor最主要的組件,然而從3.0開始RingBuffer僅僅負責存儲和更新在Disruptor中流通的數據。對一些特殊的使用場景能夠被用戶(使用其他數據結構)完全替代。
Sequence:Disruptor使用Sequence來表示一個特殊組件處理的序號。和Disruptor一樣,每個消費者(EventProcessor)都維持著一個Sequence。大部分的并發(fā)代碼依賴這些Sequence值的運轉,因此Sequence支持多種當前為AtomicLong類的特性。
Sequencer:這是Disruptor真正的核心。實現了這個接口的兩種生產者(單生產者和多生產者)均實現了所有的并發(fā)算法,為了在生產者和消費者之間進行準確快速的數據傳遞。
SequenceBarrier: 由Sequencer生成,并且包含了已經發(fā)布的Sequence的引用,這些的Sequence源于Sequencer和一些獨立的消費者的Sequence。它包含了決定是否有供消費者來消費的Event的邏輯。
WaitStrategy:決定一個消費者將如何等待生產者將Event置入Disruptor。
Event:從生產者到消費者過程中所處理的數據單元。Disruptor中沒有代碼表示Event,因為它完全是由用戶定義的。
EventProcessor:主要事件循環(huán),處理Disruptor中的Event,并且擁有消費者的Sequence。它有一個實現類是BatchEventProcessor,包含了event loop有效的實現,并且將回調到一個EventHandler接口的實現對象。
EventHandler:由用戶實現并且代表了Disruptor中的一個消費者的接口。
Producer:由用戶實現,它調用RingBuffer來插入事件(Event),在Disruptor中沒有相應的實現代碼,由用戶實現。
WorkProcessor:確保每個sequence只被一個processor消費,在同一個WorkPool中的處理多個WorkProcessor不會消費同樣的sequence。
WorkerPool:一個WorkProcessor池,其中WorkProcessor將消費Sequence,所以任務可以在實現WorkHandler接口的worker吃間移交
LifecycleAware:當BatchEventProcessor啟動和停止時,于實現這個接口用于接收通知。

應用舉例

Disruptor系統的最初設計是為了支持需要按照特定的順序發(fā)生的階段性類似流水線事件,這種需求在企業(yè)應用系統開發(fā)中并不少見。圖8顯示了標準的3級流水線。


首先,每個事件都被寫入硬盤(日志)作為日后恢復用。其次,這些事件被復制到備份服務器。只有在這兩個階段后,系統開始業(yè)務邏輯處理。

按順序執(zhí)行上次操作是一個合乎邏輯的方法,但是并不是最有效的方法。日志和復制操作可以同步執(zhí)行,因為他們互相獨立。但是業(yè)務邏輯必須在他們都執(zhí)行完后才能執(zhí)行。

LMAX 一種新的零售的金融交易平臺

通過低延遲處理大量交易,取得低延遲和高吞吐量,而且沒有并發(fā)代碼的復雜性,他們是怎么做到呢?現在LMAX已經產品化一段時間了,現在應該可以揭開其神秘而迷人的面紗了。

整體結構

從最高層次看,架構有三個部分:

  • 業(yè)務邏輯處理器business logic processor[5]
  • 輸入input disruptor
  • 輸出output disruptors

業(yè)務邏輯處理器處理所有的應用程序的業(yè)務邏輯,這是一個單線程的Java程序,純粹的方法調用,并返回輸出。不需要任何平臺框架,運行在JVM里,這就保證其很容易運行測試環(huán)境。

全部駐留在內存中

業(yè)務邏輯處理器有次序地取出消息,然后運行其中的業(yè)務邏輯,然后產生輸出事件,整個操作都是在內存中,沒有數據庫或其他持久存儲。將所有數據駐留在內存中有兩個重要好處:首先是快,沒有IO,也沒有事務,其次是簡化編程,沒有對象/關系數據庫的映射,所有代碼都是使用Java對象模型

使用基于內存的模型有一個重要問題:萬一崩潰怎么辦?電源掉電也是可能發(fā)生的,“事件”(Event Sourcing )概念是問題解決的核心,業(yè)務邏輯處理器的狀態(tài)是由輸入事件驅動的,只要這些輸入事件被持久化保存起來,你就總是能夠在崩潰情況下,根據事件重演重新獲得當前狀態(tài)。(NOSQL存儲的基于事件的事務實現)

LMAX提供業(yè)務邏輯處理的快照,從快照還原,每天晚上系統不繁忙時構建快照,重新啟動商業(yè)邏輯處理器的速度很快,一個完整的重新啟動 – 包括重新啟動JVM加載最近的快照,和重放一天事件 – 不到一分鐘。

快照雖然使啟動一個新的業(yè)務邏輯處理器的速度,但速度還不夠快,業(yè)務邏輯處理器在下午2時就非常繁忙甚至崩潰,LMAX就保持多個業(yè)務邏輯處理器同時運行,每個輸入事件由多個處理器處理,只有一個處理器輸出有效,其他忽略,如果一個處理器失敗,切換到另外一個,這種故障轉移失敗恢復是事件源驅動(Event Sourcing)的另外一個好處。

Ref:http://ifeve.com/disruptor/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容