目录
前言
大家好,我是月夜枫,今年双十一你购物了么?有没有想过为什么几千万人甚至几亿人购买淘宝的商品他们的服务不会挂掉吗?2022年淘宝双十一可以完成每秒54W的订单量,是不是很恐怖!那你就错了,国内做到的好的是12306没有之一,那么问题来了,为什么他们的服务可以抗住如此之庞大的并发量,今天就来分享一款新的中间件,那就是有着闪电缓存之称的Disruptor消息队列。
背景
Disruptor介绍
Disruptor 是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级)。
Disruptor 是一个 Java 的并发编程框架,大大的简化了并发程序开发的难度,在性能上也比 Java 本身提供的一些并发包要好,目前稳定的版本是3.3.11和3.4.2两个版本。
Disruptor是一个高性能的并发编程框架,号称 “单线程每秒可处理 600W 个订单” 的神器。本课程从高性能并发框架 Disruptor 核心知识开始学习,之后带你深度剖析底层源码,整合 Netty 实战,最后进行架构设计。 本课程目标就是让你彻底精通一个如此优秀的开源框架,让你无论是应对实际工作、还是面试晋升,都能游刃有余。
“多核危机” 驱动了并发编程的复兴,然后并发编程和一般的系统相比,复杂性有个很大梯度的上升。多线程开发很大困难在于:多个线程间存在依赖关系时,如何进行协调。依赖一方面是执行顺序的依赖,如某个线程执行需要依赖其他线程执行或其它线程的某些阶段执行结果,Java 为我们提供的解决方案是:wait/notify、lock/condition、join、yield、Semaphore、CountDownLatch、CyclicBarrier 以及 JDK7 新增的一个 Phaser 等;数据依赖主要是多个线程对同一资源并发修改导致的数据状态不一致问题,Java 中主要依靠 Lock 和 CAS 两种方案,也就是我们熟知的悲观锁、乐观锁。
然而,当你在并发编程方面慢慢有些经验并开始在项目中使用时,你会发现仅仅依赖 JDK 提供的上面所说开发工具类是远远不够的, JDK 提供的工具类都只能解决一个个功能 “点” 的问题。并发编程复杂性一个体现就是:多个顺序执行流在多核 CPU 中同时并行执行与我们已经习惯的单个数据顺序流执行的方式产生了很大的冲突。
好比:现在你开车从 A 地到 B 地去,传统的开发模式就像从 A 地到 B 地之间只存在一条公路,你只需要延着这个公路一直开下去就可以达到 B 地;假如经过多年发展,现在 A 地到 B 地横起有 10 条公路,纵起有 10 条公路,它们之间相互交叉形成错综复杂的公路网,你再开车从 A 地到 B 地就会存在太多的选择,可能从东南西北任何方向出发最终都能到达 B 地。这就体现了并发编程和传统编程复杂性的对比:传统编程由于只存在一个顺序执行流,可以很好的预判程序的执行流程;而并发编程存在太多的顺序执行流导致很难准确的预判出它们真正的执行流程,一旦出现问题也很难排查,就好比上面的例子第二种情况,你很难预判你开车的真正路线,而且可能存在每次路线都不一样情况。
我认为一个并发编程项目好坏其中一个关键核心就是:项目的整体结构是否清晰。很简单的一个例子,调用 notify () 方法唤醒挂起在指定对象上的休眠线程,如果没有一个清晰简单的架构设计,可能会导致在该对象上进行休眠的对象散落到系统中各处代码上,很难把控具体唤醒的是哪个线程从而与你的业务逻辑发生偏差导致 bug 的出现。当然,项目结构清晰在传统编程中也是非常看重的,只有结构清晰的架构才会让人易于理解,同时和他人沟通探讨时方便描述,但是在并发编程中这点尤为重要,因为并发编程的复杂性更高,没有一个清晰的结构设计,你可能经过大量测试修改暂时做出了一个看似没有 bug 的项目,但是后期需求变更或者是其他人来维护这个项目时,很难下手导致后期会引入大量的 bug,而且不利于项目功能的扩展。
常用的并发编程使用的模型有并行模型、流水线模型、生产者 / 消费者模型、Actor 模型等,采用模型设计一方面是因为这些模型都是大牛们经过长时间实际生产经验的积累总结出的并发编程方面一些好的解决方案;另一方面,采用模型设计可以解决相关人员之间沟通信息不对等问题,降低沟通学习成本。
并行模型是 JDK8 中 Stream 所采用的实现并发编程的方式,并行模型非常简单,就是为每个任务分配一个线程直到该任务执行结束,示意图如下:
并行模型太过简单导致对任务的精细化控制不足,一个任务可能会被分解为多个阶段,而每个阶段的子任务特性可能差别很大,这时并行模型就无能为力了。并行模型只适合于 CPU 密集型且任务中不含 IO 阻塞等情况的任务。这时,就演进出流水线模型,示意图如下:
流水线模型在实际的并发编程中使用比较常见,我们所说的 Pipeline 设计模型、Netty 框架等都是这一思想的体现。
生产者 / 消费者模型在并发编程中也是使用频度非常高的一个模型,生产者 / 消费者模型可以很容易地将生产和消费进行解耦,优化系统整体结构,并且由于存在缓冲区,可以缓解两端性能不匹配的问题。
Actor 模型其典型代表就是 Akka,基于 Akka 可以轻松实现一个分布式异步数据处理集群系统,非常强大,后期我们有机会可以再深入讨论下 Akka。
好了,说了这么多,终于要开始正题:Disruptor,官方宣传基于该框架构建的系统单线程可以支撑每秒处理 600 万订单,此框架真乃惊为天人。Disruptor 在生产者 / 消费者模型上获得尽量高的吞吐量和尽量低的延迟,其目标就是在性能优化方面做到极致。国内国外都存在大量的知名项目在广泛使用,比如我们所熟知的 strom 底层就依赖 Disruptor 的实现,其在并发、缓存区、生产者 / 消费者模型、事务处理等方面都存在一些性能优秀的方案,因此是非常值得深入研究的。
生产者 / 消费者模型
生产者 / 消费者模型在编程中使用频度非常高的一个模型,生产者 / 消费者模型可以很容易地将生产和消费进行解耦,优化系统整体结构,并且由于存在缓冲区,可以缓解两端性能不匹配的问题。生产者 / 消费者和我们所熟悉的设计模式中的观察者模型很相似,生产者类似于被观察者,消费者类似于观察者,被观察者的任何变动都以事件的方式通知到观察者;同理,生产者生产的数据都要传递给消费者最终都要被消费者处理。
一般项目开发中,我们可以使用 JDK 提供的阻塞队列 BlockingQueue 很简单的实现一个生产者 / 消费者模型,其中生产者线程负责提交需求,消费者线程负责处理任务,二者之间通过共享内存缓冲区进行通信。
BlockingQueue 实现类主要有两个:ArrayBlockingQueue 和 LinkedBlockingQueue,底层实现一个是基于数组的,一个是基于链表的,这种实现方式的差异导致了它们使用场景的不一样。在生产者 / 消费者模型中的缓存设计上肯定优先使用 ArrayBlockingQueue,但是查看 ArrayBlockingQueue 底层源码会发现,读写操作通过重入锁实现同步,而且读写操作使用的是同一把锁,并没有实现读写锁分离;另外,锁本身的成本还是比较高的,锁容易导致线程上下文频繁的发生切换,了解 CPU 核存储硬件架构的可能会知道,每核 CPU 都会存在一个独享的高速缓存 L1,假如线程切换到其它 CPU 上执行会导致之前 CPU 高速缓存 L1 中的数据不能再被使用,降低了高速缓存使用效率。因此,在高并发场景下,性能不是很优越。
//向Queue中写入数据
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//可中断方式获取锁,实现同步
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
//从Queue中取出数据
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//可中断方式获取锁,实现同步
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
Disruptor 消息生产模型
Producer 生产出一个消息事件 Event,需要放入到 RingBuffer 中,流程大致如下:
1、首先调用 Sequencer.next () 方法,获取 RingBuffer 上可用的序号用于将新生成的消息事件放入;
2、Sequencer 首先对 nextValue+1 代表当前需要申请的 RingBuffer 序号 (nextValue 标记了之前已经申请过的序号,nextValue+1 就是下一个可申请的序号),但是 nextValue+1 指向的 RingBuffer 槽位存放的消息可能并没有被消费,如果直接返回这个序号给生产者,就会导致生产一方将该槽位的消息事件重新填充覆盖导致之前数据丢失,这里就需要一个判断:判断申请的 RingBuffer 序号代表的槽位之前的消息事件是否已被消费,判断逻辑如下:
public long next(int n)
{
if (n < 1) //n表示此次生产者期望获取多少个序号,通常是1{
throw new IllegalArgumentException("n must be > 0");
}
long nextValue = this.nextValue;
//这里n一般是1,代表申请1个可用槽位,nextValue+n就代表了期望申请的可用槽位序号
long nextSequence = nextValue + n;
//减掉RingBuffer的bufferSize值,用于判断是否出现‘绕圈覆盖’
long wrapPoint = nextSequence - bufferSize;
//cachedValue缓存之前获取的最慢消费者消费到的槽位序号
long cachedGatingSequence = this.cachedValue;
//如果申请槽位序号-bufferSize比最慢消费者序号还大,代表生产者绕了一圈后又追赶上了消费者,这时候就不能继续生产了,否则把消费者还没消费的消息事件覆盖
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
/**
cursor代表当前已经生产完成的序号,了解多线程可见性可能会知道:
1、CPU和内存间速度不匹配,硬件架构上一般会在内存和CPU间还会存在L1、L2、L3三级缓存
2、特别是L1高速缓存是CPU间相互独立不能共享的,线程操作可以看着基于L1缓存进行操作,就会导致线程间修改不会立即被其它线程感知,只有L1缓存的修改写入到主存然后其它线程将主存修改刷新到自己的L1缓存,这时线程1的修改才会被其它线程感知到
3、线程修改对其它线程不能立即可见特别是在高并发下可能会带来些问题,JAVA中使用volatile可以解决可见性问题
4、这里就是采用UNSAFE.putLongVolatile()插入一个StoreLoad内存屏障,具体可见JMM模型,主要保证cursor的真实值对所有的消费线程可见,避免不可见下消费线程无法消费问题
*/
cursor.setVolatile(nextValue);
long minSequence;
//Util.getMinimumSequence(gatingSequences, nextValue)获取当前时刻所有消费线程中,消费最慢的序号
//上面说过cachedValue是缓存的消费者最慢的序号
//这样做目的:每次都去获取真实的最慢消费线程序号比较浪费资源,而是获取一批可用序号后,生产者只有使用完后,才继续获取当前最慢消费线程最小序号,重新获取最新资源
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
//如果获取最新最慢消费线程最小序号后,依然没有可用资源,做两件事:
// 1、唤醒waitStrategy上所有休眠线程,这里即是消费线程(避免因消费线程休眠而无法消费消息事件导致生产线程一直获取不到资源情况)
// 2、自旋休眠1纳秒
//可以看到,next()方法是一个阻塞接口,如果一直获取不到可用资源,就会一直阻塞在这里
waitStrategy.signalAllWhenBlocking();
LockSupport.parkNanos(1L);
}
//有可用资源时,将当前最慢消费线程序号缓存到cachedValue中,下次再申请时就可不必再进入if块中获取真实的最慢消费线程序号,只有这次获取到的被生产者使用完才会继续进入if块
this.cachedValue = minSequence;
}
//申请成功,将nextValue重新设置,下次再申请时继续在该值基础上申请
this.nextValue = nextSequence;
//返回申请到RingBuffer序号
return nextSequence;
}
3、申请到可用序号后,提取 RingBuffer 中该序号中的 Event,并重置 Event 状态为当前最新事件状态。
4、重置完成后,调用 Sequencer.publish () 提交序号,提交序号主要就是修改 cursor 值,cursor 标记已经生产完成序号,这样消费线程就可以来消费事件了。
@Override
public void publish(long sequence) {
//修改cursor序号,消费者就可以进行消费
cursor.set(sequence);
//唤醒消费线程,比如消费线程消息到无可用消息时可能会进入休眠状态,当放入新消息就需要唤醒休眠的消费线程
waitStrategy.signalAllWhenBlocking();
}
总结:消息事件生产主要包含三个步骤:
1、申请序号:表示从 RingBuffer 上获取可用的资源。
2、填充事件:表示获取到 RingBuffer 上可用资源后,将新事件放入到该资源对应的槽位上。
3、提交序号:表示第二部新事件放入到 RingBuffer 槽位全部完成,提交序号可供消费线程开始消费。
Disruptor 消息处理模型
消息处理端需要从 RingBuffer 中提取可用的消息事件,并注入到用户的业务逻辑中进行处理,流程大致如下:
1、消费端核心类是 EventProcessor,它实现了 Runnable 接口,Disruptor 在启动的时候会将所有注册上来的 EventProcessor 提交到线程池中执行,因此,一个 EventProcessor 可以看着一个独立的线程流用于处理 RingBuffer 上的数据。
2、EventProcessor 通过调用 SequenceBarrier.waitFor () 方法获取可用消息事件的序号,其实 SequenceBarrier 内部还是调用 WaitStrategy.waitFor () 方法,WaitStrategy 等待策略主要封装如果获取消息时没有可用消息时如何处理的逻辑信息,是自旋、休眠、直接返回等,不同场景需要使用不同策略才能实现最佳的性能。
ProcessingSequenceBarrier:
WaitStrategy waitStrategy;
Sequence dependentSequence;
boolean alerted = false;
Sequence cursorSequence;//可供消费消息的sequence
Sequencer sequencer;
/**
ProcessingSequenceBarrier中核心方法只有一个:waitFor(longsequence),
传入希望消费得到起始序号,返回值代表可用于消费处理的序号,一般返回可用序号>=sequence,
但也不一定,具体看WaitStrategy实现
* 总结:
*1、sequence:EventProcessor传入的需要进行消费的起始sequence
*2、这里并不保证返回值availableSequence一定等于given sequence,他们的大小关系取决于采用的WaitStrategy
*a.YieldingWaitStrategy在自旋100次尝试后,会直接返回dependentSequence的最小seq,这时并不保证返回值>=given sequence
* b.BlockingWaitStrategy则会阻塞等待given sequence可用为止,可用并不是说availableSequence == given sequence,而应当是指 >=
*.SleepingWaitStrategy:首选会自旋100次,然后执行100次Thread.yield(),还是不行则LockSupport.parkNanos(1L)直到availableSequence >= given sequence
*/
@Override
public long waitFor(final long sequence)throws AlertException, InterruptedException, TimeoutException{
checkAlert();
//调用WaitStrategy获取RingBuffer上可用消息序号,无可消费消息是该接口可能会阻塞,具体逻辑由WaitStrategy实现
long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
if (availableSequence < sequence){
return availableSequence;
}
//获取消费者可以消费的最大的可用序号,支持批处理效应,提升处理效率。
//当availableSequence > sequence时,需要遍历 sequence --> availableSequence,找到最前一个准备就绪,可以被消费的event对应的seq。
//最小值为:sequence-1
return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}
3、通过 waitFor () 返回的是一批可用消息的序号,比如申请消费 7 好槽位,waitFor () 返回的可能是 8 表示从 6 到 8 这一批数据都已生产完毕可以进行消费
4、EventProcessor 按照顺序从 RingBuffer 中取出消息事件,然后调用 EventHandler.onEvent () 触发用户的业务逻辑进行消息处理。
while (true){
try{
//读取可消费消息序号
final long availableSequence = sequenceBarrier.waitFor(nextSequence);
if (batchStartAware != null) {
batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
}
while (nextSequence <= availableSequence) {
//循环提取所有可供消费的消息事件
event = dataProvider.get(nextSequence);
//将提取的消息事件注入到封装用户业务逻辑的Handler中
eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
nextSequence++;
}
sequence.set(availableSequence);
}
}
}
5、当这批次的消息处理完成后,继续重复上面操作调用 waitFor () 继续获取可用的消息序号,周而复始
好了,这节主要对 Disruptor 的生产模型和消费模型进行了一个简单的介绍,后面会逐渐对 Disruptor 涉及到的每个核心组件进行分析,了解它们优秀的设计思想。
一、Disruptor 的核心概念
先从了解 Disruptor 的核心概念开始,来了解它是如何运作的。下面介绍的概念模型,既是领域对象,也是映射到代码实现上的核心对象。
1. Ring Buffer
如其名,环形的缓冲区。曾经 RingBuffer 是 Disruptor 中的最主要的对象,但从3.0版本开始,其职责被简化为仅仅负责对通过 Disruptor 进行交换的数据(事件)进行存储和更新。在一些更高级的应用场景中,Ring Buffer 可以由用户的自定义实现来完全替代。
2. Sequence Disruptor
通过顺序递增的序号来编号管理通过其进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。一个 Sequence 用于跟踪标识某个特定的事件处理者( RingBuffer/Consumer )的处理进度。
虽然一个 AtomicLong 也可以用于标识进度,但定义 Sequence 来负责该问题还有另一个目的,那就是防止不同的 Sequence 之间的CPU缓存伪共享(Flase Sharing)问题。
注:这是 Disruptor 实现高性能的关键点之一,网上关于伪共享问题的介绍已经汗牛充栋,在此不再赘述。
3. Sequencer
Sequencer 是 Disruptor 的真正核心。此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。
4. Sequence Barrier
用于保持对RingBuffer的 main published Sequence 和Consumer依赖的其它Consumer的 Sequence 的引用。Sequence Barrier 还定义了决定 Consumer 是否还有可处理的事件的逻辑。
5. Wait Strategy
定义 Consumer 如何进行等待下一个事件的策略。(注:Disruptor 定义了多种不同的策略,针对不同的场景,提供了不一样的性能表现)
6. Event
在 Disruptor 的语义中,生产者和消费者之间进行交换的数据被称为事件(Event)。它不是一个被 Disruptor 定义的特定类型,而是由 Disruptor 的使用者定义并指定。
7. EventProcessor
EventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。
8. EventHandler
Disruptor 定义的事件处理接口,由用户实现,用于处理事件,是 Consumer 的真正实现。
9. Producer
即生产者,只是泛指调用 Disruptor 发布事件的用户代码,Disruptor 没有定义特定接口或类型。
二、Disruptor 的并发框架
介绍:Disruptor 是一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的 JMS),也可以认为是一个观察者模式的实现,或者事件监听模式的实现。
作用:你可以理解为他是一种高效的 "生产者 - 消费者" 模型。也就性能远远高于传统的 BlockingQueue 容器。
用法:
第一:建立一个 Event 类
第二:建立一个工厂 Event 类,用于创建 Event 类实例对象
第三:需要有一个监听事件类,用于处理数据(Event 类)
第四:我们需要进行测试代码编写。实例化 Disruptor 实例,配置一系列参数。然后我们对 Disruptor 实例绑定监听事件类,接受并处理数据。
第五:在 Disruptor 中,真正存储数据的核心叫做 RingBuffer,我们通过 Disruptor 实例拿到它,然后把数据生产出来,把数据加入到 RingBuffer 的实例对象中即可。
备注:RingBuffer 表示环状的缓存
实现第一个 Disruptor 小例子
1. 数据对象
package com.zh;
/**
* 1.建立一个Event类(数据对象)
* @author Administrator
*
*/
public class LongEvent {
private long value;
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
}
2. 建立一个工厂 Event 类,用于创建 Event 类实例对象
package com.zh;
import com.lmax.disruptor.EventFactory;
/**
* 2.建立一个工厂Event类,用于创建Event类实例对象
* 需要让disruptor为我们创建事件,声明了一个EventFactory来实例化Event对象。
*/
public class LongEventFactory implements EventFactory {
@Override
public Object newInstance() {
return new LongEvent();
}
}
3. 需要有一个监听事件类,用于处理数据(Event 类)
package com.zh;
import com.lmax.disruptor.EventHandler;
//消费者监听,也就是一个事件处理器。该事件用于获取disruptor存储的数据。
public class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent longEvent, long l, boolean b) throws Exception {
System.out.println(longEvent.getValue());
}
}
4. 实例化 Disruptor 实例,参数配置。对 Disruptor 实例绑定监听事件类,接受并处理数据。
package com.zh;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.YieldingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
public class LongEventMain {
public static void main(String[] args) throws Exception {
// 创建线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 创建工厂
LongEventFactory factory = new LongEventFactory();
// 创建bufferSize ,也就是RingBuffer大小,必须是2的N次方
int ringBufferSize = 1024 * 1024; //
// 创建disruptor
/**
* 1.factory:工厂对象,创建数据对象
* 2.ringBufferSize:指定缓存区
* 3.executor:线程池,用于在disruptor内部数据接收处理
* 4.SINGLE或MULTI:SINGLE一个生产者,MULTI多个生产者
* 5.new YieldingWaitStrategy():disruptor策略
*/
/**
* //BlockingWaitStrategy 是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现
* WaitStrategy BLOCKING_WAIT = new BlockingWaitStrategy();
* //SleepingWaitStrategy
* 的性能表现跟BlockingWaitStrategy差不多,对CPU的消耗也类似,但其对生产者线程的影响最小,适合用于异步日志类似的场景
* WaitStrategy SLEEPING_WAIT = new SleepingWaitStrategy();
* //YieldingWaitStrategy
* 的性能是最好的,适合用于低延迟的系统。在要求极高性能且事件处理线数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,
* CPU开启超线程的特性 WaitStrategy YIELDING_WAIT = new YieldingWaitStrategy();
*/
Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(factory, ringBufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy());
// 连接消费事件方法
disruptor.handleEventsWith(new LongEventHandler());
// 启动
disruptor.start();
// Disruptor 的事件发布过程是一个两阶段提交的过程:
// 存放数据
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,方法会堵塞,直至所有的事件都得到处理;
executor.shutdown();// 关闭 disruptor 使用的线程池;如果需要的话,必须手动关闭, disruptor 在
// shutdown 时不会自动关闭;
}
}
5. 真正存储数据的核心叫做 RingBuffer,我们通过 Disruptor 实例拿到它,然后把数据生产出来,把数据加入到 RingBuffer 的实例对象中即可。
package com.zh;
import java.nio.ByteBuffer;
import com.lmax.disruptor.RingBuffer;
/**
* 当用一个简单队列来发布事件的时候会牵涉更多的细节,这是因为事件对象还需要预先创建。
* 发布事件:获取下一个事件槽并发布事件(发布事件的时候要使用try/finnally保证事件一定会被发布)。
* 如果我们使用RingBuffer.next()获取一个事件槽,那么一定要发布对应的事件。
* 如果不能发布事件,那么就会引起Disruptor状态的混乱。
* 尤其是在多个事件生产者的情况下会导致事件消费者失速,从而不得不重启应用才能会恢复。
*/
public class LongEventProducer {
private final RingBuffer<LongEvent> ringBuffer;
public LongEventProducer(RingBuffer<LongEvent> ringBuffer){
this.ringBuffer = ringBuffer;
}
/**
* onData用来发布事件,每调用一次就发布一次事件
* 它的参数会用过事件传递给消费者
*/
public void onData(ByteBuffer bb){
//1.可以把ringBuffer看做一个事件队列,那么next就是得到下面一个事件槽(环形buffer下标)
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);
}
}
}
三、Disruptor - 高性能线程消息传递框架
Disruptor 是英国 LMAX 公司开源的高性能的线程间传递消息的并发框架,和 jdk 中的 BlockingQueue 非常类似,但是性能却是 BlockingQueue 不能比拟的,下面是官方给出的一分测试报告,可以直观的看出两者的性能区别:
核心概念?
这么性能炸裂的框架肯定要把玩一番,试用前,我们先了解下 disruptor 的主要的概念,然后结合楼主的 weblog 项目(之前使用的 BlockingQueue),来实践下
RingBuffer:环形的缓冲区,消息事件信息的载体。曾经 RingBuffer 是 Disruptor 中的最主要的对象,但从 3.0 版本开始,其职责被简化为仅仅负责对通过 Disruptor 进行交换的数据(事件)进行存储和更新。在一些更高级的应用场景中,Ring Buffer 可以由用户的自定义实现来完全替代。
Event:定义生产者和消费者之间进行交换的数据类型。
EventFactory:创建事件的工厂类接口,由用户实现,提供具体的事件
EventHandler:事件处理接口,由用户实现,用于处理事件。
目前为止,我们了解以上核心内容即可,更多的详情,可以移步 wiki 文档:https://github.com/LMAX-Exchange/disruptor
核心架构图:
实践 Disruptor?
改造 boot-websocket-log 项目,这是一个典型的生产者消费者模式的实例。然后将 BlockingQueue 替换成 Disruptor,完成功能,有兴趣的可以对比下。
第一步,定义事件类型
/**
* Created by kl on 2018/8/24.
* Content :进程日志事件内容载体
*/
public class LoggerEvent {
private LoggerMessage log;
public LoggerMessage getLog() {
return log;
}
public void setLog(LoggerMessage log) {
this.log = log;
}
}
第二步,定义事件工厂
/**
* Created by kl on 2018/8/24.
* Content :进程日志事件工厂类
*/
public class LoggerEventFactory implements EventFactory{
@Override
public LoggerEvent newInstance() {
return new LoggerEvent();
}
}
第三步,定义数据处理器
/**
* Created by kl on 2018/8/24.
* Content :进程日志事件处理器
*/
@Component
public class LoggerEventHandler implements EventHandler{
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void onEvent(LoggerEvent stringEvent, long l, boolean b) {
messagingTemplate.convertAndSend("/topic/pullLogger",stringEvent.getLog());
}
}
第四步,创建 Disruptor 实操类,定义事件发布方法,发布事件
/**
* Created by kl on 2018/8/24.
* Content :Disruptor 环形队列
*/
@Component
public class LoggerDisruptorQueue {
private Executor executor = Executors.newCachedThreadPool();
// The factory for the event
private LoggerEventFactory factory = new LoggerEventFactory();
private FileLoggerEventFactory fileLoggerEventFactory = new FileLoggerEventFactory();
// Specify the size of the ring buffer, must be power of 2.
private int bufferSize = 2 * 1024;
// Construct the Disruptor
private Disruptordisruptor = new Disruptor<>(factory, bufferSize, executor);;
private DisruptorfileLoggerEventDisruptor = new Disruptor<>(fileLoggerEventFactory, bufferSize, executor);;
private static RingBufferringBuffer;
private static RingBufferfileLoggerEventRingBuffer;
@Autowired
LoggerDisruptorQueue(LoggerEventHandler eventHandler,FileLoggerEventHandler fileLoggerEventHandler) {
disruptor.handleEventsWith(eventHandler);
fileLoggerEventDisruptor.handleEventsWith(fileLoggerEventHandler);
this.ringBuffer = disruptor.getRingBuffer();
this.fileLoggerEventRingBuffer = fileLoggerEventDisruptor.getRingBuffer();
disruptor.start();
fileLoggerEventDisruptor.start();
}
public static void publishEvent(LoggerMessage log) {
long sequence = ringBuffer.next(); // Grab the next sequence
try {
LoggerEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor
// for the sequence
event.setLog(log); // Fill with data
} finally {
ringBuffer.publish(sequence);
}
}
public static void publishEvent(String log) {
if(fileLoggerEventRingBuffer == null) return;
long sequence = fileLoggerEventRingBuffer.next(); // Grab the next sequence
try {
FileLoggerEvent event = fileLoggerEventRingBuffer.get(sequence); // Get the entry in the Disruptor
// for the sequence
event.setLog(log); // Fill with data
} finally {
fileLoggerEventRingBuffer.publish(sequence);
}
}
}
以上四步已经完成了 Disruptor 的使用,启动项目后就会不断的发布日志事件,处理器会将事件内容通过 websocket 传送到前端页面上展示,
boot-websocket-log 项目地址:boot-websocket-log: 使用websocket技术实时输出系统日志到浏览器端,实现WebLog
Disruptor 是高性能的进程内线程间的数据交换框架,特别适合日志类的处理。Disruptor 也是从 https://github.com/alipay/sofa-tracer 了解到的,这是蚂蚁金服 团队开源的分布式链路追踪项目,其中日志处理部分就是使用了 Disruptor。
四、如何写一个完整的demo案例
-
添加pom.xml依赖
<dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.4</version> </dependency>
-
消息体Model
/** * 消息体 */ @Data public class MessageModel { private String message; }
-
构造EventFactory
public class HelloEventFactory implements EventFactory<MessageModel> { @Override public MessageModel newInstance() { return new MessageModel(); } }
-
构造EventHandler-消费者
@Slf4j public class HelloEventHandler implements EventHandler<MessageModel> { @Override public void onEvent(MessageModel event, long sequence, boolean endOfBatch) { try { //这里停止1000ms是为了确定消费消息是异步的 Thread.sleep(1000); log.info("消费者处理消息开始"); if (event != null) { log.info("消费者消费的信息是:{}",event); } } catch (Exception e) { log.info("消费者处理消息失败"); } log.info("消费者处理消息结束"); } }
-
构造BeanManager
/** * 获取实例化对象 */ @Component public class BeanManager implements ApplicationContextAware { private static ApplicationContext applicationContext = null; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } public static Object getBean(String name) { return applicationContext.getBean(name); } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } }
-
构造MQManager
@Configuration public class MQManager { @Bean("messageModel") public RingBuffer<MessageModel> messageModelRingBuffer() { //定义用于事件处理的线程池, Disruptor通过java.util.concurrent.ExecutorSerivce提供的线程来触发consumer的事件处理 ExecutorService executor = Executors.newFixedThreadPool(2); //指定事件工厂 HelloEventFactory factory = new HelloEventFactory(); //指定ringbuffer字节大小,必须为2的N次方(能将求模运算转为位运算提高效率),否则将影响效率 int bufferSize = 1024 * 256; //单线程模式,获取额外的性能 Disruptor<MessageModel> disruptor = new Disruptor<>(factory, bufferSize, executor, ProducerType.SINGLE, new BlockingWaitStrategy()); //设置事件业务处理器---消费者 disruptor.handleEventsWith(new HelloEventHandler()); // 启动disruptor线程 disruptor.start(); //获取ringbuffer环,用于接取生产者生产的事件 RingBuffer<MessageModel> ringBuffer = disruptor.getRingBuffer(); return ringBuffer; } }
-
构造Mqservice和实现类-生产者
public interface DisruptorMqService { /** * 消息 * @param message */ void sayHelloMq(String message); } @Slf4j @Component @Service public class DisruptorMqServiceImpl implements DisruptorMqService { @Autowired private RingBuffer<MessageModel> messageModelRingBuffer; @Override public void sayHelloMq(String message) { log.info("record the message: {}",message); //获取下一个Event槽的下标 long sequence = messageModelRingBuffer.next(); try { //给Event填充数据 MessageModel event = messageModelRingBuffer.get(sequence); event.setMessage(message); log.info("往消息队列中添加消息:{}", event); } catch (Exception e) { log.error("failed to add event to messageModelRingBuffer for : e = {},{}",e,e.getMessage()); } finally { //发布Event,激活观察者去消费,将sequence传递给改消费者 //注意最后的publish方法必须放在finally中以确保必须得到调用;如果某个请求的sequence未被提交将会堵塞后续的发布操作或者其他的producer messageModelRingBuffer.publish(sequence); } } }
-
构造测试类及方法
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = DemoApplication.class) public class DemoApplicationTests { @Autowired private DisruptorMqService disruptorMqService; /** * 项目内部使用Disruptor做消息队列 * @throws Exception */ @Test public void sayHelloMqTest() throws Exception{ disruptorMqService.sayHelloMq("消息到了,Hello world!"); log.info("消息队列已发送完毕"); //这里停止2000ms是为了确定是处理消息是异步的 Thread.sleep(2000); } }
测试运行结果
2023-04-05 14:31:18.543 INFO 7274 --- [ main] c.e.u.d.d.s.Impl.DisruptorMqServiceImpl : record the message: 消息到了,Hello world!
2023-04-05 14:31:18.545 INFO 7274 --- [ main] c.e.u.d.d.s.Impl.DisruptorMqServiceImpl : 往消息队列中添加消息:MessageModel(message=消息到了,Hello world!)
2023-04-05 14:31:18.545 INFO 7274 --- [ main] c.e.utils.demo.DemoApplicationTests : 消息队列已发送完毕
2023-04-05 14:31:19.547 INFO 7274 --- [pool-1-thread-1] c.e.u.d.disrupMq.mq.HelloEventHandler : 消费者处理消息开始
2023-04-05 14:31:19.547 INFO 7274 --- [pool-1-thread-1] c.e.u.d.disrupMq.mq.HelloEventHandler : 消费者消费的信息是:MessageModel(message=消息到了,Hello world!)
2023-04-05 14:31:19.547 INFO 7274 --- [pool-1-thread-1] c.e.u.d.disrupMq.mq.HelloEventHandler : 消费者处理消息结束
总结
其实 生成者 -> 消费者 模式是很常见的,通过一些消息队列也可以轻松做到上述的效果。不同的地方在于,Disruptor 是在内存中以队列的方式去实现的,而且是无锁的。这也是 Disruptor 为什么高效的原因。