【Java】Disruptor框架介绍

Disruptor 是一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的 JMS),也可以认为是一个观察者模式的实现,或者事件监听模式的实现。它允许开发者使用多线程技术去创建基于任务的工作流。Disruptor 能用来并行创建任务,同时保证多个处理过程的有序性。Disruptor 的目标就是快. 我们知道,java.util.concurrent.ArrayBlockingQueue 是一个非常优秀的有界队列实现。Disruptor 与之相比,性能更加的优秀。

1.背景介绍

老生重谈:锁

并发编程中,为了保证准确性,引入了锁的机制,包括乐观锁,悲观锁等。有锁就涉及到资源的竞争,竞争就可能出现死锁,这样的情况下,你只能重启你的机器了。

考虑一个简单自增的问题:

从1加到10亿,
单线程简单自增,耗时5S左右
单线程加锁自增,仅仅简单加锁,没有竞争,耗时40S左右,慢一个数量级

考虑并发
两个线程简单自增,耗时减少,但是结果无法保证准确性,比如存在脏读等问题。
两个线程加锁自增,按照理解,时间应该减半,但是因为引入了锁机制,导致竞争,实际时间更加长。

disruptor怎么做

disruptor在需要保证线程安全的地方,用到了CAS操作,这是一个CPU级别的指令,类似于乐观锁,即Campare and Set/Swap. JAVA 从1.5版本新引入了AtomicLong等支持CAS指令的数据结构。
测试代码可以看出来,在引入了AtomicLong的情况下,单线程,耗时为18S左右,多线程,耗时26S左右。

说说数据结构:链表 or 数组

既然是数据交换,那就存在一个产生数据(producer),一个消费数据(consumer),和数据存储(RingBuffer),数据存储,我们可以理解,应该是一个队列,数据先到先处理,在java类库中,提供了例如LinkedBlockingQueue、ArrayBlockingQueue等,而disruptor之所以高效,是因为它没有直接使用java类库中提供的队列,而是自己写的RingBuffer。

它有什么特点呢?

顾名思义,它是一个环,准确说,是一个用数组实现的环形队列。
不像传统队列,维护对头,队尾,它只有Sequencer,指向下一个可用的数据缓存区
新产生的数据对原来数据进行写覆盖,不进行remove操作。
队列大小一定是2的N次方
它有什么好处呢?

相比于链表,它寻址更快,时间复杂度控制在O(n)
在初始化时,就已经分配好了内存,而且新产生的只覆盖,所以更少的GC
sequece 一直自增,进行位操作可以快速定位到实际slot , sequece & (array length-1) = array index,比如一共有8槽,9 &(8-1)= 1

等待策略

在典型的消费者/生成者模型中,会存在等待现象,disruptor提供了以下的几种等待策略:

BlockingWaitStrategy 是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现
SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对 CPU 的消耗也类似,但其对生产者线程的影响最小,适合用于异步日志类似的场景;
YieldingWaitStrategy 的性能是最好的,适合用于低延迟的系统。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
经测试前两种效果差不多,延迟在微秒以内,可以忽略,cpu占用不高,YieldingWaitStrategy模式队列空闲时CPU达到100%,不适合

应用场景:

它适合一切异步环境,但是对于并发量小的场景不一定需要。在log4j2中,已经使用了disruptor进行日志记录。同样是用异步,选择disruptor会更快。

在一些获取验证码,发短信的场景下,对实时性要求不够,如果收不到,用户可以再次要求重发。
对于一些奖品,卡券的发放,在高峰期,可以只入队,在之后用异步的方式慢慢发放。
对于比较复杂的逻辑可以进行并发操作

2.框架引入

Disruptor源码地址: https://github.com/LMAX-Exchange/disruptor
仓库地址:http://mvnrepository.com/artifact/com.lmax/disruptor/3.4.2
pom.xml配置:

1
2
3    com.lmax
4    disruptor
5    3.4.2
6

3.案例分析

今天用一个停车场问题来加深对Disruptor的理解。一个有关汽车进入停车场的问题。当汽车进入停车场时,系统首先会记录汽车信息。同时也会发送消息到其他系统处理相关业务,最后发送短信通知车主收费开始。看了很多文章,里面的代码都是大同小异的,可能代码真的是很经典。以下代码也是来源网络,加了一些注释。

代码包含以下内容:
1) 事件对象Event
2)三个消费者Handler
3)一个生产者Processer
4)执行Main方法

Event类:汽车信息

1public class MyInParkingDataEvent {  
2    private String carLicense; // 车牌号  
3    public String getCarLicense() {  
4        return carLicense;  
5    }  
6    public void setCarLicense(String carLicense) {  
7        this.carLicense = carLicense;  
8    }  
9}  

Handler类:一个负责存储汽车数据,一个负责发送kafka信息到其他系统中,最后一个负责给车主发短信通知

第一个消费者,负责保存进场汽车的信息

 1import com.lmax.disruptor.EventHandler;  
 2import com.lmax.disruptor.WorkHandler;  
 3/** 
 4 * Handler 第一个消费者,负责保存进场汽车的信息 
 5 * 
 6 */  
 7public class MyParkingDataInDbHandler implements EventHandler , WorkHandler{  
 8    @Override  
 9    public void onEvent(MyInParkingDataEvent myInParkingDataEvent) throws Exception {  
10        long threadId = Thread.currentThread().getId(); // 获取当前线程id  
11        String carLicense = myInParkingDataEvent.getCarLicense(); // 获取车牌号  
12        System.out.println(String.format("Thread Id %s 保存 %s 到数据库中 ....", threadId, carLicense));  
13    }  
14    @Override  
15    public void onEvent(MyInParkingDataEvent myInParkingDataEvent, long sequence, boolean endOfBatch)  
16            throws Exception {  
17        this.onEvent(myInParkingDataEvent);  
18    }  
19}  

第二个消费者,负责发送通知告知工作人员(Kafka是一种高吞吐量的分布式发布订阅消息系统)

 1import com.lmax.disruptor.EventHandler;  
 2/** 
 3 * 第二个消费者,负责发送通知告知工作人员(Kafka是一种高吞吐量的分布式发布订阅消息系统) 
 4 */  
 5public class MyParkingDataToKafkaHandler implements EventHandler{  
 6    @Override  
 7    public void onEvent(MyInParkingDataEvent myInParkingDataEvent, long sequence, boolean endOfBatch)  
 8            throws Exception {  
 9        long threadId = Thread.currentThread().getId(); // 获取当前线程id  
10        String carLicense = myInParkingDataEvent.getCarLicense(); // 获取车牌号  
11        System.out.println(String.format("Thread Id %s 发送 %s 进入停车场信息给 kafka系统...", threadId, carLicense));  
12    }  
13}  

第三个消费者,sms短信服务,告知司机你已经进入停车场,计费开始。

 1import com.lmax.disruptor.EventHandler;  
 2/** 
 3 * 第三个消费者,sms短信服务,告知司机你已经进入停车场,计费开始。 
 4 */  
 5public class MyParkingDataSmsHandler implements EventHandler{  
 6    @Override  
 7    public void onEvent(MyInParkingDataEvent myInParkingDataEvent, long sequence, boolean endOfBatch)  
 8            throws Exception {  
 9        long threadId = Thread.currentThread().getId(); // 获取当前线程id  
10        String carLicense = myInParkingDataEvent.getCarLicense(); // 获取车牌号  
11        System.out.println(String.format("Thread Id %s 给  %s 的车主发送一条短信,并告知他计费开始了 ....", threadId, carLicense));  
12    }  
13}  

Producer类:负责上报停车数据

 1/** 
 2 * 生产者,进入停车场的车辆 
 3 */  
 4public class MyInParkingDataEventPublisher implements Runnable{  
 5    private CountDownLatch countDownLatch; // 用于监听初始化操作,等初始化执行完毕后,通知主线程继续工作  
 6    private Disruptor disruptor;  
 7    private static final Integer NUM = 10; // 1,10,100,1000  
 8    public MyInParkingDataEventPublisher(CountDownLatch countDownLatch,  
 9            Disruptor disruptor) {  
10        this.countDownLatch = countDownLatch;  
11        this.disruptor = disruptor;  
12    }  
13    @Override  
14    public void run() {  
15        MyInParkingDataEventTranslator eventTranslator = new MyInParkingDataEventTranslator();  
16        try {  
17            for(int i = 0; i < NUM; i ++) {  
18                disruptor.publishEvent(eventTranslator);  
19                Thread.sleep(1000); // 假设一秒钟进一辆车  
20            }  
21        } catch (InterruptedException e) {  
22            e.printStackTrace();  
23        } finally {  
24            countDownLatch.countDown(); // 执行完毕后通知 await()方法  
25            System.out.println(NUM + "辆车已经全部进入进入停车场!");  
26        }  
27    }  
28}  
29class MyInParkingDataEventTranslator implements EventTranslator {  
30    @Override  
31    public void translateTo(MyInParkingDataEvent myInParkingDataEvent, long sequence) {  
32        this.generateData(myInParkingDataEvent);  
33    }  
34    private MyInParkingDataEvent generateData(MyInParkingDataEvent myInParkingDataEvent) {  
35        myInParkingDataEvent.setCarLicense("车牌号: 鄂A-" + (int)(Math.random() * 100000)); // 随机生成一个车牌号  
36        System.out.println("Thread Id " + Thread.currentThread().getId() + " 写完一个event");  
37        return myInParkingDataEvent;  
38    }  
39}  

执行的Main方法:

 1import com.lmax.disruptor.EventFactory;
 2import com.lmax.disruptor.YieldingWaitStrategy;
 3import com.lmax.disruptor.dsl.Disruptor;
 4import com.lmax.disruptor.dsl.EventHandlerGroup;
 5import com.lmax.disruptor.dsl.ProducerType;
 6/**
 7 * 执行的Main方法 ,
 8 * 一个生产者(汽车进入停车场);
 9 * 三个消费者(一个记录汽车信息,一个发送消息给系统,一个发送消息告知司机)
10 * 前两个消费者同步执行,都有结果了再执行第三个消费者
11 */
12public class MyInParkingDataEventMain {
13    public static void main(String[] args) {
14        long beginTime=System.currentTimeMillis();
15        int bufferSize = 2048; // 2的N次方
16        try {
17            // 创建线程池,负责处理Disruptor的四个消费者
18            ExecutorService executor = Executors.newFixedThreadPool(4);
19            // 初始化一个 Disruptor
20            Disruptor disruptor = new Disruptor(new EventFactory() {
21                @Override
22                public MyInParkingDataEvent newInstance() {
23                    return new MyInParkingDataEvent(); // Event 初始化工厂
24                }
25            }, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy());
26            // 使用disruptor创建消费者组 MyParkingDataInDbHandler 和 MyParkingDataToKafkaHandler
27            EventHandlerGroup handlerGroup = disruptor.handleEventsWith(
28                    new MyParkingDataInDbHandler(), new MyParkingDataToKafkaHandler());
29            // 当上面两个消费者处理结束后在消耗 smsHandler
30            MyParkingDataSmsHandler myParkingDataSmsHandler = new MyParkingDataSmsHandler();
31            handlerGroup.then(myParkingDataSmsHandler);
32            // 启动Disruptor
33            disruptor.start();
34            CountDownLatch countDownLatch = new CountDownLatch(1); // 一个生产者线程准备好了就可以通知主线程继续工作了
35            // 生产者生成数据
36            executor.submit(new MyInParkingDataEventPublisher(countDownLatch, disruptor));
37            countDownLatch.await(); // 等待生产者结束
38            disruptor.shutdown();
39            executor.shutdown();
40        } catch (Exception e) {
41            e.printStackTrace();
42        }
43        System.out.println("总耗时:"+(System.currentTimeMillis()-beginTime));
44    }
45}

运行结果示意:

 1Thread Id 15 写完一个event
 2Thread Id 13 发送 车牌号: 鄂A-45365 进入停车场信息给 kafka系统...
 3Thread Id 12 保存 车牌号: 鄂A-45365 到数据库中 ....
 4Thread Id 14 给  车牌号: 鄂A-45365 的车主发送一条短信,并告知他计费开始了 ....
 5Thread Id 15 写完一个event
 6Thread Id 13 发送 车牌号: 鄂A-68983 进入停车场信息给 kafka系统...
 7Thread Id 12 保存 车牌号: 鄂A-68983 到数据库中 ....
 8Thread Id 14 给  车牌号: 鄂A-68983 的车主发送一条短信,并告知他计费开始了 ....
 9Thread Id 15 写完一个event
10Thread Id 13 发送 车牌号: 鄂A-32784 进入停车场信息给 kafka系统...
11Thread Id 12 保存 车牌号: 鄂A-32784 到数据库中 ....
12Thread Id 14 给  车牌号: 鄂A-32784 的车主发送一条短信,并告知他计费开始了 ....
13Thread Id 15 写完一个event
14Thread Id 12 保存 车牌号: 鄂A-29369 到数据库中 ....
15Thread Id 13 发送 车牌号: 鄂A-29369 进入停车场信息给 kafka系统...
16Thread Id 14 给  车牌号: 鄂A-29369 的车主发送一条短信,并告知他计费开始了 ....
17Thread Id 15 写完一个event
18Thread Id 13 发送 车牌号: 鄂A-42107 进入停车场信息给 kafka系统...
19Thread Id 12 保存 车牌号: 鄂A-42107 到数据库中 ....
20Thread Id 14 给  车牌号: 鄂A-42107 的车主发送一条短信,并告知他计费开始了 ....
21Thread Id 15 写完一个event
22Thread Id 13 发送 车牌号: 鄂A-97497 进入停车场信息给 kafka系统...
23Thread Id 12 保存 车牌号: 鄂A-97497 到数据库中 ....
24Thread Id 14 给  车牌号: 鄂A-97497 的车主发送一条短信,并告知他计费开始了 ....
25Thread Id 15 写完一个event
26Thread Id 12 保存 车牌号: 鄂A-17216 到数据库中 ....
27Thread Id 13 发送 车牌号: 鄂A-17216 进入停车场信息给 kafka系统...
28Thread Id 14 给  车牌号: 鄂A-17216 的车主发送一条短信,并告知他计费开始了 ....
29Thread Id 15 写完一个event
30Thread Id 12 保存 车牌号: 鄂A-94315 到数据库中 ....
31Thread Id 13 发送 车牌号: 鄂A-94315 进入停车场信息给 kafka系统...
32Thread Id 14 给  车牌号: 鄂A-94315 的车主发送一条短信,并告知他计费开始了 ....
33Thread Id 15 写完一个event
34Thread Id 13 发送 车牌号: 鄂A-12107 进入停车场信息给 kafka系统...
35Thread Id 12 保存 车牌号: 鄂A-12107 到数据库中 ....
36Thread Id 14 给  车牌号: 鄂A-12107 的车主发送一条短信,并告知他计费开始了 ....
37Thread Id 15 写完一个event
38Thread Id 13 发送 车牌号: 鄂A-96823 进入停车场信息给 kafka系统...
39Thread Id 12 保存 车牌号: 鄂A-96823 到数据库中 ....
40Thread Id 14 给  车牌号: 鄂A-96823 的车主发送一条短信,并告知他计费开始了 ....
4110辆车已经全部进入进入停车场!
42总耗时:10044

参考文章:
https://www.jianshu.com/p/d3e5915a7ac5
http://ifeve.com/disruptor-dsl/

你可能感兴趣的:(JAVA)