LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现
解决的问题
当我们有多个消息的生产者线程,一个消费者线程时,他们之间如何进行高并发、线程安全的协调?
很简单,用一个队列。
当我们有多个消息的生产者线程,多个消费者线程,并且每一条消息需要被所有的消费者都消费一次(这就不是一般队列,只消费一次的语义了),该怎么做?
这时仍然需要一个队列。但是:
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点所提到的),记录最后一条可以被消费的消息。这个全局指针就在下图红框中。
生产场景实现
生产者往RingBuffer中发送一条消息(RingBuffer.publish())时:
1. 生产者的私有sequence会+1
2. 检查生产者的私有sequence与RingBuffer中Event个数的关系。如果发现Event数组满了(下图红框中的判断),则阻塞(下图绿框中的等待)。
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中的所有消息了。那么当前消费者会阻塞。等待生产者加入更多的消息:
以上代码中,红框中的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消费业务逻辑。
最后,上图中的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))。——都是套路:)
对象的复用
JVM运行时,一怕创建大对象,二怕创建很多小对象。这都会导致JVM堆碎片化、对象元数据存储的额外开销大。这是高性能Java应用的噩梦。
为了解决第二点“很多小对象”,主流开源框架都会自己维护、复用对象池。LMAX Disruptor也不例外。
生产者不是创建新的Event对象,放入到RingBuffer中。而是从RingBuffer中取出一个已有的Event对象,更新它所指向的业务数据,来代表一个逻辑上的新Event。
所以LMAX Disruptor的生产者API,用起来有些麻烦——分为三步,一是下图绿框中取出一个已有的、已经被所有人消费过的Event对象,二是下图红框中更新这个Event对象所指向的业务数据,三是下图蓝框中标记这个Event对象为逻辑上的新Event。
总结
https://github.com/LMAX-Exchange/disruptor/wiki/Introduction 这篇文章对Disruptor基本概念已经介绍得很清楚了。
但是,我觉得,入门介绍结合源码去咀嚼,才会比较sexy,朋友们会深入理解。其实也不难,关键是找出源码中的核心部分。
篇幅所限,本文对于Disruptor的高级功能没有解释,比如处理多个消费者之间的依赖关系。有机会补充。
LMAX Disruptor—多生产者多消费者中,消息复制分发的高性能实现的更多相关文章
- 使用Disruptor实现生产者和消费者模型
生产者 package cn.lonecloud.procum.disruptor; import cn.lonecloud.procum.Data; import com.lmax.disrupto ...
- disruptor 单生产者多消费者
demo1 单生产者多消费者创建. maven 依赖 <!-- https://mvnrepository.com/artifact/com.lmax/disruptor --> < ...
- disruptor 多生产者多消费者实战 四
一.创建event类 Order public class Order { private String id; private String name; private double price; ...
- Disrunptor多生产者多消费者模型讲解
多生产者多消费者模拟需求:1.创建100个订单生产者,每个生产者生产100条订单,总共会生产10000条订单,由3个消费者进行订单消费处理.2.100个订单生产者全部创建完毕,再一起生产消费订单数据 ...
- Disruptor多个消费者不重复处理生产者发送过来的消息
1.定义事件事件(Event)就是通过 Disruptor 进行交换的数据类型. package com.ljq.disruptor; import java.io.Serializable; /** ...
- Disruptor框架中生产者、消费者的各种复杂依赖场景下的使用总结
版权声明:原创作品,谢绝转载!否则将追究法律责任. Disruptor是一个优秀的并发框架,可以实现单个或多个生产者生产消息,单个或多个消费者消息,且消费者之间可以存在消费消息的依赖关系.网上其他博客 ...
- disruptor架构四 多生产者多消费者执行
1.首先介绍下那个时候使用RingBuffer,那个时候使用disruptor ringBuffer比较适合场景比较简单的业务,disruptor比较适合场景较为复杂的业务,很多复杂的结果必须使用di ...
- 十五、.net core(.NET 6)搭建RabbitMQ消息队列生产者和消费者的简单方法
搭建RabbitMQ简单通用的直连方法 如果还没有MQ环境,可以参考上一篇的博客,在windows系统上的rabbitmq环境搭建.如果使用docker环境,可以直接百度一下,应该就一个语句就可以搞定 ...
- 【MQ】java 从零开始实现消息队列 mq-02-如何实现生产者调用消费者?
前景回顾 上一节我们学习了如何实现基于 netty 客服端和服务端的启动. [mq]从零开始实现 mq-01-生产者.消费者启动 [mq]java 从零开始实现消息队列 mq-02-如何实现生产者调用 ...
随机推荐
- Word2013中制作按钮控件
1.由于“开发工具”不经常用,所以在功能选项面板中没有“开发工具”这一栏.所以我们需要设置.在功能选项面板中选择“文件”,在跳转出来的版面中选择“选项”.
- javascript中通过匿名函数进行事件绑定
- paper 121 :matlab中imresize函数
转自:http://www.cnblogs.com/rong86/p/3558344.html matlab中函数imresize简介: 函数功能:该函数用于对图像做缩放处理. 调用格式: B = i ...
- webapi返回json格式优化
一.设置webapi返回json格式 在App_Start下的WebApiConfig的注册函数Register中添加下面这代码 config.Formatters.Remove(config.For ...
- iframe载入页面过程显示动画效果
http://www.xfeixiang.com/Bug/Detail/A2FD7EFFA8CA72F1
- ubuntu安装cpu版caffe
最近在笔记本上配置了ubuntu14.04,并配置了caffe,整个过程大概花了2个小时. 希望在安装时能给大家一个启发,这里配置的是无gpu版的,因为我的笔记本时核心显卡,配置gpu版的要编译cud ...
- uboot补丁的分析
接下来分析一下韦老师的uboot补丁: -------------------------------------------------------------------------------- ...
- 004_Intelij 使用,Anonymous Apex
插件安装好之后,就可以正常添加cloud 项目: 注意:免费使用是30天,为了不去买license,在过期后,去修改下机器的日期,这个日期是在安装后的一个月内,改好日期后,启动Intelij 少一个截 ...
- 学习Linux系列--Python资源收集
本系列文章记录了个人学习过程的点点滴滴. 回到目录 玩Linux,不懂一门脚本语言是不行的,我这里选择的是Python. 10.入门资源 下面前三遍是比较系统的文档,后面是一些心得,我看了好几天,终于 ...
- windows平台下基于VisualStudio的Clang安装和配置
LLVM 是一个开源的编译器架构,它已经被成功应用到多个应用领域.Clang是 LLVM 的一个编译器前端,它目前支持 C, C++, Objective-C 以及 Objective-C++ 等编程 ...