java multiline 13: disruptor framework

Disruptor series (1)-introduction to disruptor

This article is translated from Disruptor's wiki article Introduction on github. You can read the original text here.


### I. Introduction

Most programmers are obsessed with technology and want to improve in this area. Maintain an active learning mentality for outstanding things. Concurrent programming is a major problem in development. Whether it is the various theories at the bottom or the implementation of concurrent components at the top, it is very difficult to understand. The reason why concurrency is difficult is that it is difficult to control because of "many" . Most of them will use the "lock" technology for control, but the "lock" technology often runs counter to performance. In order to maximize performance, lock-free unlocking is obviously the key to improving concurrent performance. The wisdom of the predecessors is really unfathomable, and it really has made a breakthrough in this field, using a different design method from the past to achieve a lock-free high-performance concurrent queue-  disruptor .

This series will learn about disruptor:

  • Introduction to diruptor
  • disruptor use
  • Principle of disruptor
  • Use of disruptor in open source

The series will learn from the above aspects from the shallower to the deeper, layer by layer. The disruptor is open source, and the source code is hosted on github LMAX-Exchange/disruptor .

Among them, the wiki has many excellent articles, which are very good learning resources. But they are all in English, so I simply translate the introduction and usage articles here .


### Two. What is

LMAX Disruptor is a high-performance message library for thread internal communication. It originated from LMAX's research on concurrency, performance, and non-blocking algorithms, and has now become a core part of LMAX's transaction infrastructure.

The best way to understand what a Disruptor is is to compare it with the existing ones that are easier to understand. Disruptor is equivalent to BlockingQueue in Java. Like queues, the purpose of Disruptor is to exchange data between threads in the same process. However, Disruptor provides some key features different from queues:

  • Broadcast events to consumers and follow consumer dependencies
  • Pre-allocate memory for events
  • Optional lock-free


### Three. Core Concepts

Before understanding how Disruptor works, it is very valuable to define some terms that are ubiquitous in documents and source code. For those who prefer DDD, they are the ubiquitous language in the Disruptor field.

  • Ring Buffer : RingBuffer is the main concept of Disruptor. But since Diruptor 3.0, RingBuffer is only responsible for storing and updating Disruptor data. Some advanced usage scenarios can be replaced by users.

  • Sequence (sequence) : Disruptor uses Sequence to mark where a specific component arrives. Each consumer (EventProcessor) maintains a Sequence internally to mark the location of its consumption. Most concurrent code relies on the movement of Sequence, so Sequence supports a large number of AtomicLong features. In fact, the difference between the two is that Sequence implements additional functions to prevent false sharing.

  • Sequencer : Sequencer is the actual core of Disruptor. Two of the implementations (Single Producer, Multi Producer) all implement algorithms for fast and correct data transfer between producers and consumers.

  • Sequence Barrier (sequence barrier) : Sequence Barrier is created by Sequencer. It contains a reference to the published main sequence from the Sequencer, or contains the sequence of dependent consumers. At the same time, it also contains the logic to determine whether there is time to reach the total consumer processing.

  • Wait Strategy : Wait Strategy determines the way consumers wait for the producer to put the event into the Disruptor.

  • Event : The unit of data passed from the producer to the consumer. Event is usually defined by the user, and there is no specific code specification.

  • Event Processor : The main event loop that processes events from the Disruptor, and contains the consumer's sequence. One is implemented as BatchEventProcessor, which contains an efficient event loop and will continuously call back the provided EventHandler interface.

  • EventHandler (event processing logic) : an interface that defines the processing logic of the consumer by the user.

  • Producer (producer) : This is also the user code used to implement, call the Disruptor into the team event. There is no code specification.

In order to be able to associate these concepts, that is to say in a context, the following figure is an example to show the scene of LMAX using Disruptor in high-performance core services:

Note:
There is no explanation for this picture in the original text. When I saw it, the author looked dazed. I also studied this picture after understanding the Disruptor globally to understand its meaning. This picture can be described as a vivid depiction of Disruptor.
The author does not introduce it too much here, because to understand this picture, it is necessary to have an overall understanding of Disruptor. We will analyze the principle in detail later.
In general, the Producer production events are put into the RingBuffer, and the Consumer uses the Sequence Barrier and its own Sequence to obtain consumable events from the RingBuffer.


### Four. Features

1. Broadcast events

This is a huge behavioral difference between queue and Disruptor. When you have multiple consumers listening on the same Disruptor, all events will be sent to all consumers. This is different from queues, in which only one consumer will be sent at a time. Disruptor is more preferred to be used in scenarios where there are multiple non-dependent consumers processing the same data in parallel. There is a very classic case in LMAX, there are three operations journalling (write input data to a persistent log file), replication (send input data to another machine to ensure remote backup) and business logic processing. Similar to Executor-style event processing, where different events are processed in parallel at the same time, you can use WorkePool.

Looking at the above picture again, there are three event processors (JournalConsumer, ReplicationConsumer and ApplicationConsumer) listening to the Disruptor, and each of them will receive all available messages in the Disruptor. This allows three consumers to work in parallel.

2. Consumer dependency graph

In order to support the practical application of parallel processing behavior, it is necessary to support the previous coordination of consumers. Taking the above example again, the processing of business logic must be completed after journalling and replication. We call this concept gating, or more accurately the superset of this behavior feature is called gating. In Disruptor, Gating occurs in two places. First, we need to ensure that producers do not surpass consumers. This is done by calling RingBuffer.addGatingConsumers() to add related consumers to the Disruptor. The second is the aforementioned scenario, which is realized by constructing a SequenceBarrier that contains a sequence of consumers that must be completed first.

To quote the above figure, there are three consumers listening to events from RingBuffer. In this example, there is a dependency graph. ApplicationConsumer depends on JournalConsumer and ReplicationConsumer. This means that JournalConsumer and ReplicationConsumer can freely run concurrently. The dependency can be seen as the connection from the SequenceBarrier of the ApplicationConsumer to the Sequence of the JournalConsumer and ReplicationConsumer. Another point worth paying attention to is the relationship between Sequencer and downstream consumers. Its role is to ensure that the release does not wrap RingBuffer. In order to achieve this, none of the downstream consumer's Sequence is lower than the RingBuffer's Sequence rather than the size of the RingBuffer. Then when using dependencies, an interesting optimization can be used. Because the Sequence of ApplicationConsumers is guaranteed to be lower than or equal to the Sequence of JournalConsumer and ReplicationConsumer, the Sequencer only needs to check the Sequence of ApplicationConsumers. In a more general application scenario, the Sequencer only needs to be aware of the Sequence of the leaf nodes in the consumer tree.

3. Event pre-allocation

One of the goals of Disruptor is to be used in a low-latency environment. In a low-latency system, memory allocation must be reduced or eliminated. In a Java-based system, it is necessary to reduce the number of pauses due to GC (in a low-latency C/C++ system, due to the contention of the memory allocator, a large amount of memory allocation can also cause problems).

To meet this, users can pre-allocate memory for events in the Disruptor. During construction, the EventFactory is provided by the user and will be called for each entry in the RingBuffer of the Disruptor. When publishing new data to the Disruptor, the API allows the user to obtain an object that has been constructed so that it can call methods or update the object's domain. Disruptor will ensure that these operations are thread-safe.

4. Optional lock-free

The demand for low latency has pushed another key factor to be the widespread use of lock-free algorithms to implement Disruptor. All memory visibility and correctness are achieved using memory barriers and CAS operations. Only one scenario uses lock in BlockingWaitStrategy. This is just to use conditions to park consuming threads while waiting for new events to arrive. Many low-latency systems use busy and so on to avoid jitter caused by the use of Condition. But the number of busy waits will cause performance degradation, especially when CPU resources are severely limited. For example, a web server in a virtual environment.

Disruptor series (two) usage scenarios

Today I use an order question to deepen my understanding of Disruptor. When an order is generated in the system, the system first records the order information. At the same time, messages will be sent to other systems to process related services, and finally the order is processed.

The code contains the following:

1) Event object Event

2) Three consumer Handler

3) A Producer

4) Execute the Main method

1. Order processing system code

(1) Event

public class Trade {  
    
    private String id;//ID  
    private String name;
    private double price;//金额  
    private AtomicInteger count = new AtomicInteger(0);
    
    // 省略getter/setter
}  

(2)  Handler class

One is responsible for storing order information, one is responsible for sending Kafka information to other systems, and the last is responsible for processing order information.

import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.WorkHandler;

/**
 * 第一个 Handler1,存储到数据库中
 */
public class Handler1 implements EventHandler<Trade>, WorkHandler<Trade> {
      
    @Override  
    public void onEvent(Trade event, long sequence, boolean endOfBatch) throws Exception {  
        this.onEvent(event);  
    }  
  
    @Override  
    public void onEvent(Trade event) throws Exception {
        long threadId = Thread.currentThread().getId();     // 获取当前线程id
        String id = event.getId();                          // 获取订单号
        System.out.println(String.format("%s:Thread Id %s 订单信息保存 %s 到数据库中 ....",
                this.getClass().getSimpleName(), threadId, id));
    }  
}  
import com.lmax.disruptor.EventHandler;

/**
 * 第二个 Handler2,订单信息发送到其它系统中
 */
public class Handler2 implements EventHandler<Trade> {  
      
    @Override  
    public void onEvent(Trade event, long sequence,  boolean endOfBatch) throws Exception {
        long threadId = Thread.currentThread().getId();     // 获取当前线程id
        String id = event.getId();                          // 获取订单号
        System.out.println(String.format("%s:Thread Id %s 订单信息 %s 发送到 karaf 系统中 ....",
                this.getClass().getSimpleName(), threadId, id));
    }
}    
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.WorkHandler;

/**
 * 第三个 Handler2,处理订单信息
 */
public class Handler3 implements EventHandler<Trade>, WorkHandler<Trade> {

    @Override  
    public void onEvent(Trade event, long sequence,  boolean endOfBatch) throws Exception {
        onEvent(event);
    }

    @Override
    public void onEvent(Trade event) throws Exception {
        long threadId = Thread.currentThread().getId();     // 获取当前线程id
        String id = event.getId();                          // 获取订单号
        System.out.println(String.format("%s:Thread Id %s 订单信息 %s 处理中 ....",
                this.getClass().getSimpleName(), threadId, id));
    }
} 

(3)  Producer class

import com.lmax.disruptor.EventTranslator;
import com.lmax.disruptor.dsl.Disruptor;

import java.util.UUID;
import java.util.concurrent.CountDownLatch;

public class TradePublisher implements Runnable {  
    
    Disruptor<Trade> disruptor;  
    private CountDownLatch latch;  
    
    private static int LOOP = 1;    // 模拟百万次交易的发生
  
    public TradePublisher(CountDownLatch latch, Disruptor<Trade> disruptor) {
        this.disruptor=disruptor;  
        this.latch=latch;  
    }  
  
    @Override  
    public void run() {  
        TradeEventTranslator tradeTransloator = new TradeEventTranslator();  
        for(int i = 0; i < LOOP; i++) {
            disruptor.publishEvent(tradeTransloator);  
        }  
        latch.countDown();  
    }  
      
}  
  
class TradeEventTranslator implements EventTranslator<Trade>{
    
    @Override  
    public void translateTo(Trade event, long sequence) {
        event.setId(UUID.randomUUID().toString());
    }
    
}  

(4)  Main method executed

package com.github.binarylei.disruptor.demo3;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.EventHandlerGroup;
import com.lmax.disruptor.dsl.ProducerType;

public class Main {  
    public static void main(String[] args) throws InterruptedException {  
       
        long beginTime=System.currentTimeMillis();  
        int bufferSize=1024;  
        ExecutorService executor=Executors.newFixedThreadPool(8);

        Disruptor<Trade> disruptor = new Disruptor<>(new EventFactory<Trade>() {
            @Override  
            public Trade newInstance() {  
                return new Trade();  
            }  
        }, bufferSize, executor, ProducerType.SINGLE, new BusySpinWaitStrategy());  
        
        //菱形操作
        //使用disruptor创建消费者组C1,C2  
        EventHandlerGroup<Trade> handlerGroup = 
                disruptor.handleEventsWith(new Handler1(), new Handler2());
        //声明在C1,C2完事之后执行JMS消息发送操作 也就是流程走到C3 
        handlerGroup.then(new Handler3());

        disruptor.start();//启动
        CountDownLatch latch=new CountDownLatch(1);  
        //生产者准备  
        executor.submit(new TradePublisher(latch, disruptor));
        
        latch.await();//等待生产者完事. 
       
        disruptor.shutdown();  
        executor.shutdown();  
        System.out.println("总耗时:"+(System.currentTimeMillis()-beginTime));  
    }  
}  

The test results are as follows:

Handler1:Thread Id 10 订单信息保存 a097c77d-08f1-430a-8342-2143963f268f 到数据库中 ....
Handler2:Thread Id 11 订单信息 a097c77d-08f1-430a-8342-2143963f268f 发送到 karaf 系统中 ....
Handler3:Thread Id 13 订单信息 a097c77d-08f1-430a-8342-2143963f268f 处理中 ....
总耗时:1631

You can see that Handler3 is executed after Handler1 and Handler2 are executed.

Two, Disruptor DSL

Although the disruptor pattern is simple to use, it requires too much boilerplate code to establish multiple consumers and their dependencies. In order to be able to quickly and easily apply to 99% of the scenes, I prepared a simple domain specific language (DSL) for the Disruptor mode, which defines the order of consumption. More Disruptor scene use

Before explaining Disruptor DSL, let's take a look at the problem of non-repetitive consumption by multiple consumers.

2.1 Multiple consumers do not repeat consumption

The default is one consumer and one thread. If you want to realize that multiple consumers of C3 do not repeatedly consume data, you can use handlerGroup.thenHandleEventsWithWorkerPool(customers)

//使用disruptor创建消费者组C1, C2  
EventHandlerGroup<Trade> handlerGroup = disruptor.handleEventsWith(new Handler1(), new Handler2());

// 多个消费者不重复消费
Handler3[] customers = new Handler3[]{new Handler3(), new Handler3(), new Handler3()};
handlerGroup.thenHandleEventsWithWorkerPool(customers);

2.2 Consumer’s "Quadrangle Model"

Quadrilateral pattern

In this case, as long as the producer (P1) puts elements on the ring buffer, consumers C1 and C2 can process these elements in parallel. But consumer C3 must wait until C1 and C2 are processed before it can be processed. The corresponding case in the real world is like: Before processing the actual business logic (C3), you need to verify the data (C1) and write the data to the disk (C2).

//1. 使用disruptor创建消费者组C1,C2  
EventHandlerGroup<Trade> handlerGroup = disruptor.handleEventsWith(new Handler1(), new Handler2());

//2. 声明在C1,C2完事之后执行JMS消息发送操作 也就是流程走到C3 
handlerGroup.then(new Handler3());

2.3 Consumer's "Sequential Execution Mode"

disruptor.handleEventsWith(new Handler1()).
    handleEventsWith(new Handler2()).
    handleEventsWith(new Handler3());

2.4 Consumers’ "hexagonal pattern"

We can even build a parallel consumer chain in a more complex hexagonal pattern:

Hexagon pattern

 

 

 

Handler1 h1 = new Handler1();
Handler2 h2 = new Handler2();
Handler3 h3 = new Handler3();
Handler4 h4 = new Handler4();
Handler5 h5 = new Handler5();
disruptor.handleEventsWith(h1, h2);
disruptor.after(h1).handleEventsWith(h4);
disruptor.after(h2).handleEventsWith(h5);
disruptor.after(h4, h5).handleEventsWith(h3);

 

Dissecting the Disruptor: why is it so fast

  1. Dissecting the Disruptor: Why is it so fast? (1) Disadvantages of locks
  2. Dissecting the Disruptor: Why is it so fast? (2) Magical cache line filling
  3. Dissecting the Disruptor: Why is it so fast? (3) False sharing
  4. Dissecting the Disruptor: Why is it so fast? (4) Demystifying the memory barrier

How the Disruptor works and uses

  1. How to use Disruptor (1) The special features of Ringbuffer
  2. How to use Disruptor (2) How to read from Ringbuffer
  3. How to use Disruptor (three) to write to Ringbuffer
  4. Analyze the Disruptor relationship assembly
  5. Disruptor (lock-free concurrency framework)-release
  6. LMAX Disruptor-a high-performance, low-latency and simple framework
  7. The Disruptor Wizard is dead, and the Disruptor Wizard will live forever!
  8. Disruptor 2.0 update summary
  9. There is no need to compete for data sharing between threads

Application of Disruptor

  1. LMAX architecture
  2. Process 1M tps through Axon and Disruptor

Guess you like

Origin blog.csdn.net/zhaofuqiangmycomm/article/details/113804600