Disruptor 系列(一)快速入门

Disruptor:是一个开源的并发框架,能够在 无锁 的情况下实现网络的 Queue 并发操作,所以处理数据的能力比 Java 本身提供的并发类容器要大的多,在一个线程里每秒处理 6 百万订单。 可以把它当作一个轻量级的 MQ 和无锁的 BlockingQueue。

一、BlockingQueue

jdk 常用的队列有

队列 有界性 数据结构
ArrayBlockingQueue bounded 加锁 arraylist
LinkedBlockingQueue optionally-bounded 加锁 linkedlist
ConcurrentLinkedQueue unbounded 无锁 linkedlist
LinkedTransferQueue unbounded 无锁 linkedlist
PriorityBlockingQueue unbounded 加锁 heap
DelayQueue unbounded 加锁 heap

考虑到内存回收,防止无界队列内存溢出,通常使用 ArrayBlockingQueue。常用于生产者-消费者模式:

二、Disruptor 特点

  • Disruptor 是一个 Java 的并发编程框架,大大的简化了并发程序开发的难度,在性能上也比 Java 本身提供的一些并发包要好。

  • Disruptor 是一个高性能异步处理框架,它实现了观察者模式,或者事件监听模式的实现。

  • Disruptor 是无锁的、CPU 友好,它不会清除缓存中的数据,只会覆盖,降低了垃圾回收机制启动的频率。

三、HelloWorld 代码

可以通过 Maven安装Disruptor。查看 Getting-Started 快速入门,更多API

<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.7</version>
</dependency>

我们从一个简单的例子开始学习 Disruptor:生产者传递一个 long 类型的值给消费者,而消费者消费这个数据的方式仅仅是把它打印出来。首先声明一个 Event 来包含需要传递的数据:

public class LongEvent {
private long value;
public long getValue() {
return value;
} public void setValue(long value) {
this.value = value;
}
}

由于需要让 Disruptor 为我们创建事件,我们同时还声明了一个 EventFactory 来实例化 Event 对象。

public class LongEventFactory implements EventFactory {
@Override
public Object newInstance() {
return new LongEvent();
}
}

我们还需要一个事件消费者,也就是一个事件处理器。这个事件处理器简单地把事件中存储的数据打印到终端:

public class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent longEvent, long l, boolean b) throws Exception {
System.out.println(longEvent.getValue());
}
}

事件都会有一个生成事件的源,可以简单的理解为一个事件生产者。这个例子中假设事件是由于磁盘IO或者network读取数据的时候触发的,事件源使用一个ByteBuffer来模拟它接受到的数据,也就是说,事件源会在IO读取到一部分数据的时候触发事件(触发事件不是自动的,程序员需要在读取到数据的时候自己触发事件并发布):

public class LongEventProducer {
private final RingBuffer<LongEvent> ringBuffer;
public LongEventProducer(RingBuffer<LongEvent> ringBuffer){
this.ringBuffer = ringBuffer;
} /**
* onData用来发布事件,每调用一次就发布一次事件
* @param ByteBuffer 它的参数会用过事件传递给消费者
*/
public void onData(ByteBuffer bb){
//1.可以把ringBuffer看做一个事件队列,那么next就是得到下面一个事件槽
long sequence = ringBuffer.next();
try {
//2.用上面的索引取出一个空的事件用于填充(获取该序号对应的事件对象)
LongEvent event = ringBuffer.get(sequence);
//3.获取要通过事件传递的业务数据
event.setValue(bb.getLong(0));
} finally {
//4.发布事件
//注意,最后的 ringBuffer.publish 方法必须包含在 finally 中以确保必须得到调用;
// 如果某个请求的 sequence 未被提交,将会堵塞后续的发布操作或者其它的 producer。
ringBuffer.publish(sequence);
}
}
}

很明显的是:当用一个简单队列来发布事件的时候会牵涉更多的细节,这是因为事件对象还需要预先创建。发布事件最少需要两步:获取下一个事件槽并发布事件( 发布事件的时候要使用 try/finally 保证事件一定会被发布 )。如果我们使用 RingBuffer.next() 获取一个事件槽,那么一定要发布对应的事件。如果不能发布事件,那么就会引起 Disruptor 状态的混乱。尤其是在多个事件生产者的情况下会导致事件消费者失速,从而不得不重启应用才能会恢复。

Disruptor 3.0 提供了 lambda 式的 API。这样可以把一些复杂的操作放在 Ring Buffer,所以在 Disruptor3.0 以后的版本最好使用 Event Publisher 或者 Event Translator 来发布事件。

public class LongEventProducerWithTranslator {
//一个translator可以看做一个事件初始化器,publicEvent方法会调用它
private static final EventTranslatorOneArg<LongEvent, ByteBuffer> TRANSLATOR =
new EventTranslatorOneArg<LongEvent, ByteBuffer>() {
public void translateTo(LongEvent event, long sequence, ByteBuffer buffer) {
event.setValue(buffer.getLong(0));
}
}; private final RingBuffer<LongEvent> ringBuffer;
public LongEventProducerWithTranslator(RingBuffer<LongEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void onData(ByteBuffer buffer) {
ringBuffer.publishEvent(TRANSLATOR, buffer);
}
}

最后一步就是把所有的代码组合起来完成一个完整的事件处理系统。

public class LongEventMain {
public static void main(String[] args) throws Exception {
//1. 创建disruptor
Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>( // (1)
new LongEventFactory(), // 创建工厂
1024 * 1024, // RingBuffer 大小,必须是2的N次方
Executors.defaultThreadFactory(), // 线程池
ProducerType.SINGLE, // 单个生产者,如果有多个生产者必须使用 ProducerType.MULTI
new YieldingWaitStrategy() // 生产者和消费者的平衡策略
); //2. 连接消费事件方法
disruptor.handleEventsWith(new LongEventHandler());
//3. 启动
disruptor.start(); //4. 发布事件
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); //LongEventProducer producer = new LongEventProducer(ringBuffer);
LongEventProducerWithTranslator producer = new LongEventProducerWithTranslator(ringBuffer);
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
for(long l = 0; l<100; l++){
byteBuffer.putLong(0, l);
producer.onData(byteBuffer);
//Thread.sleep(1000);
} disruptor.shutdown();//关闭 disruptor,方法会堵塞,直至所有的事件都得到处理;
}
}
  1. Disruptor 构造方法如下:

    new Disruptor(
    EventFactory<T> eventFactory, // 事件工厂类
    int ringBufferSize, // RingBuffer 大小
    ThreadFactory threadFactory, // ThreadFactory
    ProducerType producerType, // 生产模式,ProducerType.SINGLE ProducerType.MULTI
    WaitStrategy waitStrategy) // 等待策略,3种
  2. WaitStrategy 策略类型:

    • BlockingWaitStrategy 是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现

    • SleepingWaitStrategy 的性能表现跟BlockingWaitStrategy差不多,对CPU的消耗也类似,但其对生产者线程的影响最小,适合用于异步日志类似的场景

    • YieldingWaitStrategy 的性能是最好的,适合用于低延迟的系统。在要求极高性能且事件处理线数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性

四、Disruptor 组件说明

从生产者-消费者的整体:

  • RingBuffer: 被看作 Disruptor 最主要的组件,然而从 3.0 开始 RingBuffer 仅仅负责存储和更新在 Disruptor 中流通的数据。对一些特殊的使用场景能够被用户(使用其他数据结构)完全替代。更多Ringbuffer

  • Sequence: Disruptor 使用 Sequence 来表示一个特殊组件处理的序号。和 Disruptor 一样,每个消费者(EventProcessor)都维持着一个 Sequence。大部分的并发代码依赖这些 Sequence 值的运转,因此 Sequence 支持多种当前为 AtomicLong 类的特性。

  • Producer:由用户实现,它调用 RingBuffer 来插入事件(Event)。在 Disruptor 中没有相应的实现代码,由用户实现。

  • Event:从生产者到消费者过程中所处理的数据单元。在 Disruptor 中没有相应的实现代码,由用户定义的。

  • EventHandler:由用户实现并且代表了 Disruptor 中的一个消费者的接口。

从 Disruptor 框架如何处理 Event 的细节:

  • Sequencer: 这是 Disruptor 真正的核心。实现了这个接口的两种生产者(单生产者和多生产者)均实现了所有的并发算法,为了在生产者和消费者之间进行准确快速的数据传递。通过复杂的算法去协调生存者和消费者之间的关系。

  • SequenceBarrier: Sequecer具体的实施者,由 Sequencer 生成。它包含了决定是否有供消费者来消费的 Event 的逻辑。(生产者发布事件快于消费,生产者等待。消费速度大于生产者发布事件速度,消费者监听)

  • EventProcessor:主要事件循环,处理 Disruptor 中的 Event,并且拥有消费者的 Sequence。它有一个实现类是 BatchEventProcessor,包含了event loop有效的实现,并且将回调到一个 EventHandler 接口的实现对象。

  • WorkProcessor:确保每个 sequence 只被一个 processor 消费,在同一个 WorkPool 中的处理多个 WorkProcessor 不会消费同样的 sequence。

  • LifecycleAware:当 BatchEventProcessor 启动和停止时,实现这个接口用于接收通知。

  • WaitStrategy:当消费者等待在SequenceBarrier上时,有许多可选的等待策略。

    • BusySpinWaitStrategy : 自旋等待,类似 Linux Kernel 使用的自旋锁。低延迟但同时对 CPU 资源的占用也多。
    • BlockingWaitStrategy : 使用锁和条件变量。CPU 资源的占用少,延迟大。
    • SleepingWaitStrategy : 在多次循环尝试不成功后,选择让出 CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。这种策略平衡了延迟和 CPU 资源占用,但延迟不均匀。
    • YieldingWaitStrategy : 在多次循环尝试不成功后,选择让出 CPU,等待下次调。平衡了延迟和 CPU 资源占用,但延迟也比较均匀。
    • PhasedBackoffWaitStrategy : 上面多种策略的综合,CPU 资源的占用少,延迟大。

参考:

https://blog.csdn.net/bohu83/article/details/78273246


每天用心记录一点点。内容也许不重要,但习惯很重要!

Disruptor 系列(一)快速入门的更多相关文章

  1. SpringBoot系列: RestTemplate 快速入门

    ====================================相关的文章====================================SpringBoot系列: 与Spring R ...

  2. Flask开发系列之快速入门

    Flask开发系列之快速入门 文档 一个最小的应用 调试模式 路由 变量规则 构造 URL HTTP 方法 静态文件 模板渲染 访问请求数据 环境局部变量 请求对象 文件上传 Cookies 重定向和 ...

  3. BIML 101 - ETL数据清洗 系列 - BIML 快速入门教程 - 序

    BIML 101 - BIML 快速入门教程 做大数据的项目,最花时间的就是数据清洗. 没有一个相对可靠的数据,数据分析就是无木之舟,无水之源. 如果你已经进了ETL这个坑,而且预算有限,并且有大量的 ...

  4. BIML 101 - ETL数据清洗 系列 - BIML 快速入门教程 - 连接数据库执行SQL语句

    BIML 101 - BIML 快速入门教程 第一节 连接数据库执行SQL语句 本小节将用BIML建一个简单的可以执行的包. 新建一个biml文件,贴入下面的代码 1 <Biml xmlns=& ...

  5. Maven系列之快速入门

    文章结构 唯快不破---Maven快速入门 稳打稳扎---Maven核心知识 实用为先---Maven如何建立Web项目  1   唯快不破---Maven快速入门       1.1 Maven项目 ...

  6. Quartz.NET开源作业调度框架系列(一):快速入门step by step

    Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...

  7. SpringBoot系列: JdbcTemplate 快速入门

    对于一些小的项目, 我们没有必要使用MyBatis/JPA/Hibernate等重量级技术, 直接使用Spring JDBC 即可, Spring JDBC 是对 jdbc的简单封装, 很容易掌握. ...

  8. Zookeeper系列1 快速入门

    Zookeeper的简介这里我就不说了,在接下来的几篇文章会涉及zookeeper环境搭建,watcher以及相关配置说明, 三种操作zookeeper的方式(原生API方式,zkclient,Cur ...

  9. Quartz.NET开源作业调度框架系列(一):快速入门step by step-转

    Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...

  10. BIML 101 - ETL数据清洗 系列 - BIML 快速入门教程 - 将文本文件(csv)数据导进数据库

    第二节 将文本文件数据导进数据库 该小节介绍如何用BIML生成ssis包,将货币文本导入到数据库currency的表中. SSIS组件: Connection Manager组建管理connectio ...

随机推荐

  1. 使用python获取路径问题

    import sys curDir = sys.path[0] sys.path.append(curDir + '\\models') sys.path.append(curDir + '/mode ...

  2. POI2011题解

    POI2011题解 2214先咕一会... [BZOJ2212][POI2011]Tree Rotations 线段树合并模板题. #include<cstdio> #include< ...

  3. 【LGR-051】洛谷9月月赛

    [LGR-051]洛谷9月月赛 luogu 签到题 description 给出\(K\)和质数\(m\),求最小的\(N\)使得\(111....1\)(\(N\)个\(1\))\(\equiv k ...

  4. 前端安全系列:如何防止CSRF攻击?

    背景 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点.在移动互联网时代,前端人员除了传统的 XSS.CSRF 等安全问题之外,又时常遭遇网络劫持 ...

  5. Outlook中在Exchange服务器无法保存邮件副本

    最近帮同事设置Outlook2007,结果她直接登录公司网页Exchange,发现存在Exchange上的邮件副本全没了,原以为是Outlook邮箱账号设置里”保存服务器项副本“没打勾,后来才发现账号 ...

  6. Windows 2008 关闭远程桌面的单用户多会话模式

    Windows 2008 关闭远程桌面的单用户多会话模式 在腾讯云上购买了一台云服务器. 因为设置了自动登录,在远程桌面连接后会启动一个新的会话,然后软件被运行了两次,端口被占用,无法起动. 还有可能 ...

  7. C#细说多线程(下)

    本文主要从线程的基础用法,CLR线程池当中工作者线程与I/O线程的开发,并行操作PLINQ等多个方面介绍多线程的开发. 其中委托的BeginInvoke方法以及回调函数最为常用.而 I/O线程可能容易 ...

  8. <meta name="viewport" content="width=device-width, initial-scale=1.0">的说明

    今天在做适配手机版时,chrome调到手机版,但是还是显示PC端的样式,无法展现出手机端的样式: 开始的时候还以为是chrome版本的问题,最新版本的chrome62.0是有很多变化的,而之前工作中使 ...

  9. jsp中遇到Integer的方法valueOf()和parseInt()的区别.前者要求是对象类型,后者是数字型字符串

    他们有本质区别,Integer.valueof(String s)是将一个包装类是将一个实际值为数字的变量先转成string型再将它转成Integer型的包装类对象(相当于转成了int的对象)这样转完 ...

  10. Django将.csv文件(excel文件)显示到网页上

    今天,我成功将项目要导入的测试数据导入并呈现了,虽然还不是很完美,但我之后仍会继续改进. 1.首先在主页面上加一个超链接按钮: 其它的不需要管,其它是我的另一个项目,没什么大用的 2.之后配置URL: ...