LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现

摘要:
总而言之,如果有多个生产者,多个消费者,并且同一条消息要给到所有的消费者都去处理一下,需要做到以上4点。LMAXDisruptor,正是这种场景下,满足以上4点要求的单机跨线程消息传递、分发的开源、高性能实现。这里有一篇英文的Disruptor介绍好文:https://github.com/LMAX-Exchange/disruptor/wiki/Introduction关键概念1.RingBuffer应用需要传递的消息在Disrutpor中称为Event(事件)。

解决的问题

当我们有多个消息的生产者线程,一个消费者线程时,他们之间如何进行高并发、线程安全的协调?

很简单,用一个队列。

当我们有多个消息的生产者线程,多个消费者线程,并且每一条消息需要被所有的消费者都消费一次(这就不是一般队列,只消费一次的语义了),该怎么做?

这时仍然需要一个队列。但是:

1. 每个消费者需要自己维护一个指针,知道自己消费了队列中多少数据。这样同一条消息,可以被多个人独立消费。

2. 队列需要一个全局指针,指向最后一条被所有生产者加入的消息。消费者在消费数据时,不能消费到这个全局指针之后的位置——因为这个全局指针,已经是代表队列中最后一条可以被消费的消息了。

3. 需要协调所有消费者,在消费完所有队列中的消息后,阻塞等待。

4. 如果消费者之间有依赖关系,即对同一条消息的消费顺序,在业务上有固定的要求,那么还需要处理谁先消费,谁后消费同一条消息的问题。

总而言之,如果有多个生产者,多个消费者,并且同一条消息要给到所有的消费者都去处理一下,需要做到以上4点。这是不容易的。

LMAX Disruptor,正是这种场景下,满足以上4点要求的单机跨线程消息传递、分发的开源、高性能实现。

这里有一篇英文的Disruptor介绍好文:https://github.com/LMAX-Exchange/disruptor/wiki/Introduction

关键概念

1. RingBuffer

应用需要传递的消息在Disrutpor中称为Event(事件)。

RingBuffer是Event的数组,实现了阻塞队列的语义:

如果RingBuffer满了,则生产者会阻塞等待。

如果RingBuffer空了,则消费者会阻塞等待。

2. Sequence

在上文中,我提到“每个消费者需要自己维护一个指针”。这里的指针就是一个单调递增长整数(及其基于CAS的加法、获取操作),称为Sequence。

除了每个消费者需要维护一个指针外,RingBuffer自身也要维护一个全局指针(如上一节第2点所提到的),记录最后一条可以被消费的消息。这个全局指针就在下图红框中。

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第1张

生产场景实现

生产者往RingBuffer中发送一条消息(RingBuffer.publish())时:

1. 生产者的私有sequence会+1

2. 检查生产者的私有sequence与RingBuffer中Event个数的关系。如果发现Event数组满了(下图红框中的判断),则阻塞(下图绿框中的等待)。

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第2张

3. RingBuffer会在Event数组中(sequencer+1) % BUFFER_SIZE的地方,放入Event。这里的取模操作,就体现了Event数组用到最后,则回到头部继续放,所谓“Ring“ Buffer的轮循复用语义。

消费场景实现

消费者从RingBuffer循环队列中获取一条消息时:

1. 从消费者私有Sequence,可以知道它自己消费到了RingBuffer队列中的哪一条消息。

2. 从RingBuffer的全局指针Sequence,可以知道RingBuffer中最后一条没有被消费的消息在什么位置。

3. N = (RuingBuffer的全局指针Sequence -消费者私有Sequence),就是当前消费者,还可以消费多少Event。

4. 如果以上差值N为0,说明当前消费者已经消费过RingBuffer中的所有消息了。那么当前消费者会阻塞。等待生产者加入更多的消息:

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第3张

以上代码中,红框中的availableSequence就是RingBuffer的全局指针Sequence。绿框中的sequence是当前消费者的私有sequence。

如果这个判断为true,说明RingBuffer中最新一条可以被消费的Event,已经被当前消费者消费过了。那么就会调用apployWaitMethod()阻塞,等待生产者产生更多的Event。

5. 如果RingBuffer中,还有可以被当前消费者消费的Event,即N > 0,

那么消费者,会一口气获取所有可以被消费的N个Event。即下图中的while循环,直到N个Event都被消费才退出。这种一口气消费尽量多的Event,是高性能的体现。

从RingBuffer中每获取一个Event,都会回调绿框中的eventHandler——这是应用注册的Event处理方法,执行应用的Event消费业务逻辑。

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第4张

最后,上图中的sequence.set(availableSequence),会把当前消费者的私有Sequence更新到RingBuffer的全局Sequence。表示RingBuffer中所有的Event都已经消费掉了。

高性能的实现细节

无锁

无锁就没有锁竞争。当生产者、消费者线程数很高时,意义重大。所以,

往大里说,每个消费者维护自己的Sequence,基本没有跨线程共享的状态。

往小里说,Sequence的加法是CAS实现的。

  • 当生产者需要判断RingBuffer是否已满时,用CAS比较原先RingBuffer的Event个数,和假定放入新Event后Event的个数。
  • 如果CAS返回false,说明在判断期间,别的生产者加入了新Event;或者别的消费者拿走了Event。那么当前判断无效,需要重新判断。这就是常见的 do { ... } while (false == CAS(oldVal, newVal))。——都是套路:)

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第5张

对象的复用

JVM运行时,一怕创建大对象,二怕创建很多小对象。这都会导致JVM堆碎片化、对象元数据存储的额外开销大。这是高性能Java应用的噩梦。

为了解决第二点“很多小对象”,主流开源框架都会自己维护、复用对象池。LMAX Disruptor也不例外。

生产者不是创建新的Event对象,放入到RingBuffer中。而是从RingBuffer中取出一个已有的Event对象,更新它所指向的业务数据,来代表一个逻辑上的新Event。

所以LMAX Disruptor的生产者API,用起来有些麻烦——分为三步,一是下图绿框中取出一个已有的、已经被所有人消费过的Event对象,二是下图红框中更新这个Event对象所指向的业务数据,三是下图蓝框中标记这个Event对象为逻辑上的新Event。

LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现第6张

总结

https://github.com/LMAX-Exchange/disruptor/wiki/Introduction这篇文章对Disruptor基本概念已经介绍得很清楚了。

但是,我觉得,入门介绍结合源码去咀嚼,才会比较sexy,朋友们会深入理解。其实也不难,关键是找出源码中的核心部分。

篇幅所限,本文对于Disruptor的高级功能没有解释,比如处理多个消费者之间的依赖关系。有机会补充。

免责声明:文章转载自《LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Linux下常用小工具、命令行偏前端 + rsa加解密 + jsencrypt.min.js--(新增超长字符分段加解密)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

ubuntu下DNS原理及相关设置

1.DNS原理分析如下: 当 DNS 客户机需要查询程序中使用的名称时,它会查询本地DNS 服务器来解析该名称。客户机发送的每条查询消息都包括3条信息,以指定服务器应回答的问题。● 指定的 DNS 域名,表示为完全合格的域名 (FQDN) 。● 指定的查询类型,它可根据类型指定资源记录,或作为查询操作的专门类型。● DNS域名的指定类别。对于DNS 服务器...

Linux内核数据结构hlist_head

参考自:https://blog.csdn.net/zhanglei4214/article/details/6767288 一、hlist结构简介 hlist_head 和 hlist_node 是位于linux内核中的数据结构,其设计初衷主要是为了减少Hash表的内存消耗。 structhlist_head { struct hlist_node...

VBA在32位下没问题,到64位异常报错

64 位 Visual Basic for Applications 概述Microsoft Visual Basic for Applications (VBA) 是 Microsoft Office 附带的 Visual Basic 版本。在 Microsoft Office 2010 中,VBA 包括可使 VBA 代码同时在 32 位和 64 位环境...

Jackson总结:常用注解、整合spring、自定义JsonSerializer

转自Jackson总结:常用注解、整合spring、自定义JsonSerializer Jackson作为springMVC默认的MessageConverter(消息序列化工具),经常在项目中使用,如果熟悉Jackson常用的使用方法,特性化机制,就会事半功倍,极大提高前后端数据交互的灵活性。 maven依赖 <dependency>...

多线程:C#线程同步lock,Monitor,Mutex,同步事件和等待句柄

转自:http://www.cnblogs.com/freshman0216/archive/2008/07/29/1252253.html 本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开始, 希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而对每种方式的使用细...

@EnableAsync@Async使用总结

我们在使用多线程的时候,往往需要创建Thread类,或者实现Runnable接口,如果要使用到线程池,我们还需要来创建Executors,在使用spring中,已经给我们做了很好的支持。只要要@EnableAsync就可以使用多线程。使用@Async就可以定义一个线程任务。通过spring给我们提供的ThreadPoolTaskExecutor就可以使用线...