我是「 kangarooking(袋鼠帝) 」 真心给大家分享经验和技术干货,面试次数100+。面试经验绝对丰富,也当过面试官,内容绝对用心可靠。点赞在看,养成习惯。关注me,每天进步亿点点 ❗ ❗ ❗
2022-6-23
前言
本章主要讲Disruptor的一些优秀的设计思想,我们主要学习它的设计思想和实现思路。本文对Disruptor的学习起到一个抛砖引玉的作用,也集合了个人学习过程中查阅的一些资料。通过对本章以及相关资料的学习(当然最主要的还是要自己动手去实践,debug体会),相信足以对Disruptor这个高性能队列有一个全面且深入的认识。
简单介绍一下概念
Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。简单来说就是java的高性能内存队列,即在同一个JVM进程中的多线程间消息传递,引入依赖就能使用(这是目前的最新版本)。
<!-- 高性能内存队列 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
说一下优势
据目前资料显示:应用Disruptor的知名项目有如下的一些:Storm,Camel,Log4j2,shenyu还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。
- 使用广泛
- 高性能
- 无锁机制
- 轻量
- 支持多种生产消费模式
为啥子高性能
- 通过填充
cpu缓存行,解决cpu伪共享问题; - 预分配内存,在初始化的时候将环形数组内填充满对象。使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收;
- 使用环形队列(环形数组),在一块连续的内存空间,利用缓存行的一次性加载
64k的内存,提高了cpu缓存命中率; - 使用
cas,无锁保证原子性; - 使用环形数组,只新增和覆盖数据。数组长度2^n, 通过位运算, 加快定位的速度, 下标采取递增的形式. 不用担心index溢出的问题。 index是long类型, 即使100万QPS的处理速度, 也需要30万年才能用完;
Disruptor3.x中,序号栅栏SequenceBarrier和序号Sequence搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。
Disruptor3.x中,各个消费者和生产者持有自己的序号,这些序号的变化必须满足如下基本条件:
- 消费者序号数值必须小于生产者序号数值
- 消费者序号数值必须小于其前置(依赖关系)消费者的序号数值
- 生产者序号数值不能大于消费者中最小的序号数值,以避免生产者速度过快,将还没来得及消费的消息覆盖
相关类以及属性介绍
有助于看源码时理解主要的一些属性的作用(这块可以阅读源码的时候在看)。
| 类 | 功能 |
|---|---|
| AbstractSequencer | 记录生产者当前的进度,是生产者的游标 |
| WorkPool | 共同消费者池,类似线程池管理 |
| WorkProcessor | 共同消费者,每个共同消费者共同消费整个数据,解决不重复消费者场景 |
| BatchEventProcessor | 独立消费者,每个独立消费者独自消费整个数据 |
| ProcessingSequenceBarrier | 消费者的等待策略 |
| MultiProducerSequencer | 用于统计当前环形数组中哪些位置是被写入的 |
| 所属类 | 属性 | 功能 |
|---|---|---|
| AbstractSequencer | cursor | 记录生产者当前的进度,是生产者的游标 |
| AbstractSequencer | Sequence[] gatingSequences | 存放WorkPool.workSequence以及各WorkProcessor.sequence的对象引用 |
| WorkPool | workSequence | 记录消费者当前进度 |
| WorkProcessor | workSequence | 获取消费者当前进度(序号),或将要消费的序号,和WorkPool.workSequence是同一个 |
| WorkProcessor | sequence | 存放消费者已经消费到的最新序号,WorkPool.sequence使用的同一个实例 |
| WorkProcessor | sequenceBarrier | 每个WorkProcessor(共同消费者)的SequenceBarrier实例是同一个 |
| BatchEventProcessor | sequenceBarrier | BatchEventProcessor(独立消费者)的SequenceBarrier实例是同一个 |
| BatchEventProcessor | sequence | BatchEventProcessor(独立消费者)的sequence实例是独有的 |
| ProcessingSequenceBarrier | waitStrategy | 消费者的等待策略 |
| ProcessingSequenceBarrier | dependentSequence | 如果消费者不依赖于其它的消费者,值同cursorSequence |
| ProcessingSequenceBarrier | cursorSequence | 和上面的cursor是同一个引用 |
| ProcessingSequenceBarrier | sequencer | 等于上面的AbstractSequencer |
| MultiProducerSequencer | int[] availableBuffer | 用于统计当前环形数组中哪些位置是被写入的 |
| MultiProducerSequencer | gatingSequenceCache | 存放消费者最近消费的最小序号,在发现追尾的时候去更新它,而不是直接去消费者那边获取,减少因为消费者序号经常改变缓存行失效导致的伪共享问题 |
| AbstractSequencer | Sequence[] gatingSequences | 存放所有消费者的sequence,对于共同消费者,它还存放了WorkPool的workSequence |
WorkProcessor(共同消费者):多个消费者消费的时候不重复消费;
BatchEventProcessor(独立消费者):多个消费者消费的时候每个消费者都消费全量数据。
DEMO代码
定义消息类
/**
* 消息
*/
@Data
public class OrderEvent {
private int order;
private Long orderId;
}
定义消息工厂类,实现disruptor内部的接口EventFactory
/**
* 消息工厂
*/
public class OrderEventFactory implements EventFactory<OrderEvent> {
/**
* 在disruptor初始化过程中,预分配内存时被调用
* @return
*/
@Override
public OrderEvent newInstance() {
return new OrderEvent();
}
}
定义消费者,实现EventHandler接口,也可以实现WorkHandler接口。区别是EventHandler实现的每个消费者都会消费全量数据,而WorkHandler实现的所有消费者共同来消费全量数据。
/**
* 消费者
*/
public class OrderEventHandler implements EventHandler<OrderEvent> {
@Override
public void onEvent(OrderEvent orderEvent, long sequence, boolean endOfBatch) throws Exception {
System.out.println("orderEvent=" + orderEvent + " sequence=" + sequence + " endOfBatch=" + endOfBatch);
}
}
初始化并启动disruptor,注入消费者们,同时开启一个生产者。
public class OrderEventMain {
public static void main(String[] args) throws Exception {
int bufferSize = 16;
//初始化disruptor队列
Disruptor<OrderEvent> disruptor = new Disruptor<>(new OrderEventFactory(), bufferSize, DaemonThreadFactory.INSTANCE);
//初始化两种消费者
OrderEventHandler orderEventHandler1 = new OrderEventHandler();
//初始化两个独立消费者
disruptor.handleEventsWith(orderEventHandler1, (e, l, end) -> {
System.out.println("order=" + e.getOrder());
Thread.sleep(500);
});
//初始化三个共同消费者
EventHandlerGroup<OrderEvent> orderEventEventHandlerGroup = disruptor.handleEventsWithWorkerPool((orderEvent) -> {
System.out.println("不可重复消费1 消息=" + orderEvent.getOrder());
}, (orderEvent) -> {
System.out.println("不可重复消费2 消息=" + orderEvent.getOrder());
},
(orderEvent) -> {
System.out.println("不可重复消费3 消息=" + orderEvent.getOrder());
});
// .thenHandleEventsWithWorkerPool(new OrderEventWorkHandler());
//下面是生产者,往disruptor里面丢数据
RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
//开启所有消费者监听数据
disruptor.start();
ByteBuffer bb = ByteBuffer.allocate(8);
for (int i = 0; i < 1000; i++) {
bb.putLong(0, i);
int finalI = i;
ringBuffer.publishEvent((event, sequence, buffer) -> {
event.setOrderId(buffer.getLong(0));
event.setOrder(finalI);
}, bb);
Thread.sleep(300);
}
}
}
庖丁解牛
初始化disruptor队列
从OrderEventMain的new Disruptor<>(new OrderEventFactory(), bufferSize, DaemonThreadFactory.INSTANCE);debug下来找到这个方法。
public static <E> RingBuffer<E> createMultiProducer(
EventFactory<E> factory,
int bufferSize,
WaitStrategy waitStrategy)
{
MultiProducerSequencer sequencer = new MultiProducerSequencer(bufferSize, waitStrategy);
return new RingBuffer<E>(factory, sequencer);
}
public MultiProducerSequencer(int bufferSize, final WaitStrategy waitStrategy)
{
super(bufferSize, waitStrategy);
availableBuffer = new int[bufferSize];
indexMask = bufferSize - 1;
indexShift = Util.log2(bufferSize);
initialiseAvailableBuffer();
}
对availableBuffer进行初始化,用于后面消费者判断当前消费进度是否超过生产进度,如果超过就执行相应的等待策略。
private void initialiseAvailableBuffer()
{
for (int i = availableBuffer.length - 1; i != 0; i--)
{
setAvailableBufferValue(i, -1);
}
setAvailableBufferValue(0, -1);
}
初始化时都是-1,当生产者生产第一圈数据时,会逐序将0-16下标的-1更新为0,生产第二圈数据时逐序将0-16下标的0更新为1,这样消费者就能感知生产者生产的进度了。
创建RingBuffer
RingBufferFields(
EventFactory<E> eventFactory,
Sequencer sequencer)
{
this.sequencer = sequencer;
this.bufferSize = sequencer.getBufferSize();
if (bufferSize < 1)
{
throw new IllegalArgumentException("bufferSize must not be less than 1");
}
if (Integer.bitCount(bufferSize) != 1)
{
throw new IllegalArgumentException("bufferSize must be a power of 2");
}
this.indexMask = bufferSize - 1;
this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
fill(eventFactory);
}
通过调用EventFactory实现类的newInstance方法来填充数组,从而达到预分配内存的作用。这里也是给开发者留的一个扩展点,可以在newInstance方法中自定义预分配对象的样子。
通过填充缓存行,来解决伪共享问题。
不理解为什么它左右都填充数据的同学参考:www.jianshu.com/p/e1a1b950f…
注入及启动消费者(以共同消费者为列)
handleEventsWithWorkerPool其实就是把这个消费者对象放入disruptor内部的集合中。
直接看start,最终会调用到这个方法,也就是循环从消费者集合中获取消费者,然后执行,消费者查看调用关系实际上是Runable的子类。
我们以WorkProcessor为例,查看它的run方法
public void run()
{
if (!running.compareAndSet(false, true))
{
throw new IllegalStateException("Thread is already running");
}
sequenceBarrier.clearAlert();
//这里类似spring的Aware,是留给开发者的扩展点,可以在消费的开始做一些自定义操作
notifyStart();
//已处理序列(流程控制,判断当前序列的消息是否已被处理)
boolean processedSequence = true;
//缓存的可用序列
long cachedAvailableSequence = Long.MIN_VALUE;
//下一个序列
long nextSequence = sequence.get();
T event = null;
//循环从队列中获取数据
while (true)
{
try
{
// if previous sequence was processed - fetch the next sequence and set
// that we have successfully processed the previous sequence
// typically, this will be true
// this prevents the sequence getting too far forward if an exception
// is thrown from the WorkHandler
if (processedSequence)
{
processedSequence = false;
do
{
//获取消费者当前的进度并+1,得到本次要消费者的序号
//nextSequence是将要消费,但是还未实际消费的序号
nextSequence = workSequence.get() + 1L;
//sequence里面存放消费者当前的进度
sequence.set(nextSequence - 1L);
}
//cas判断当前消费者是否可以获得nextSequence的消费权
while (!workSequence.compareAndSet(nextSequence - 1L, nextSequence));
}
//cachedAvailableSequence代表申请到的最大消费序号
//nextSequence要小于这个序号才能执行消费逻辑
if (cachedAvailableSequence >= nextSequence)
{
event = ringBuffer.get(nextSequence);
workHandler.onEvent(event);
processedSequence = true;
}
else
{
//否则就要等待生产者跟上生产进度
//并重新获取一段空间进行消费(这段空间就是nextSequence到cachedAvailableSequence之间的序号范围)
cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence);
}
}
catch (final TimeoutException e)
{
notifyTimeout(sequence.get());
}
catch (final AlertException ex)
{
if (!running.get())
{
break;
}
}
catch (final Throwable ex)
{
// handle, mark as processed, unless the exception handler threw an exception
exceptionHandler.handleEventException(ex, nextSequence, event);
processedSequence = true;
}
}
notifyShutdown();
running.set(false);
}
五种WaitStrategy(等待策略):
-
BlockingWaitStrategy:默认策略,没有获取到任务的情况下线程会进入等待状态。cpu消耗少,但是延迟高。 -
TimeoutBlockingWaitStrategy:相对于BlockingWaitStrategy来说,设置了等待时间,超过后抛异常。 -
BusySpinWaitStrategy:线程一直自旋等待。cpu占用高,延迟低. -
YieldingWaitStrategy:尝试自旋 100 次,然后调用Thread.yield()让出cpu。cpu占用高,延迟低。 -
SleepingWaitStrategy:尝试自旋 100 此,然后调用Thread.yield()100 次,如果经过这两百次的操作还未获取到任务,就会尝试阶段性挂起自身线程。此种方式是对cpu占用和延迟的一种平衡,性能不太稳定。
生产者
生产数据就两步:
1.获取当前能生产数据的序号;
2.往这个序号生产数据,其实就是为已经实例化的对象赋值。
生产者包含:多生产者模型(MultiProducerSequencer)和单生产者模型(SingleProducerSequencer)。
这里以多生产者模型(MultiProducerSequencer)为例
public <A> void publishEvent(EventTranslatorOneArg<E, A> translator, A arg0)
{
//获取当前能生产数据的序号
final long sequence = sequencer.next();
//往这个序号生产数据,其实就是为已经实例化的对象赋值。
translateAndPublish(translator, sequence, arg0);
}
public long next(int n)
{
if (n < 1)
{
throw new IllegalArgumentException("n must be > 0");
}
long current;
long next;
do
{
current = cursor.get();
next = current + n;
// 可能构成环路的点/环形缓冲区可能追尾的点 = 请求的序号 - 环形缓冲区大小
long wrapPoint = next - bufferSize;
// 缓存的消费者们的最慢进度值,小于等于真实进度
long cachedGatingSequence = gatingSequenceCache.get();
// 第一步:空间不足就继续等待。
// 1.wrapPoint > cachedGatingSequence 表示生产者追上消费者产生环路,上次看见的序号缓存无效,即缓冲区已满,此时需要获取消费者们最新的进度,以确定是否队列满。
// 2.cachedGatingSequence > current 表示消费者的进度大于当前生产者进度,表示current无效,有以下可能:
// 2.1 其它生产者发布了数据,并更新了gatingSequenceCache,并已被消费(当前线程进入该方法时可能被挂起,重新恢复调度时看见一个更大值)。
// 2.2 claim的调用(建议忽略)
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current)
{
// 走进这里表示cachedGatingSequence过期或current过期,此时都需要获取最新的gatingSequence
long gatingSequence = Util.getMinimumSequence(gatingSequences, current);
// 消费者最新的进度仍然与当前生产者构成了环路,那么只能重试
if (wrapPoint > gatingSequence)
{
// wrapPoint > gatingSequence 意味着 gatingSequence无效,因为生产者期待的是一个大于等于wrapPoint的值,因此也就不更新缓存。
LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
continue;
}
// 检测到未构成环路(多线程下这都是假设条件),更新网关序列,然后进行重试
// 这里存在竞态条件,多线程模式下,可能会被设置为多个线程看见的结果中的任意一个,可能会被设置为一个更小的值,从而小于当前的查询值
gatingSequenceCache.set(gatingSequence);
// 这里看见有足够空间,这里如果尝试竞争空间会产生重复的代码,其实就是外层的代码,因此直接等待下一个循环
}
else if (cursor.compareAndSet(current, next))
{
// 第三步:成功竞争到了这片空间,返回
// 注意!这里更新了生产者进度,然而生产者并未真正发布数据。
// 因此消费者需要调用getHighestPublishedSequence()确认真正的可用空间
break;
}
}
while (true);
return next;
}
便于理解生产消费如何获取一段空间可参考:blog.csdn.net/zhouzhenyon…
应用场景
- 当java的内存列队性能成为瓶颈的时候;
- 当需要多线程消费又要保证数据最终的顺序性时;
- 实现
mysqlbinlog高性能顺序性同步(developer.aliyun.com/article/944… - 支持多边形的消费模型,定制化的链式消费(blog.csdn.net/boling_cava…
使用方式
简单使用就直接封装一个工具类出来,想要扩展性,可以参考shenyu封装一个成jar包(一个依赖)。
总结
最后对上面的内容做一个小结:
首先总结了它高性能的原因,列出了相关类以及属性(这块有助于看源码的时候快速理解相关代码)。
然后给出了demo示例,介绍了disruptor的使用,大家可以根据这个demo来debug源码。
最后根据demo示例剖析了disruptor的初始化过程,以及消费者的注入和开启,最后到生产者如何生产数据(并没有达到庖丁解牛,只是抛砖引玉)。消费者使用sequenceBarrier.waitFor(nextSequence);方法,通过序号栅栏SequenceBarrier和序号Sequence搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。
中间以及结尾穿插了很多网址,这些网址对大家理解disruptor源码,以及它的企业级使用有很大帮助。
最后讲一下学习disruptor的好处:如果你的项目刚好有它的使用场景,就可以赶紧用起来。如果使用disruptor出现问题,可以快速定位。可以借鉴它的思想来实现自己程序的优化。最后面试的时候还能和面试官吹一吹,岂不美哉!
继续深入学习可以参考:
- blog.csdn.net/zhouzhenyon…
- baijiahao.baidu.com/s?id=166050…
- blog.csdn.net/boling_cava…
- blog.csdn.net/qq_40378034…
- blog.csdn.net/qq_40378034…
参考的源码中文注释项目地址:github.com/hl845740757…
微信公众号「 袋鼠先生的客栈 」,有问题评论区见。如果你觉得我的分享对你有帮助,或者觉得我有两把刷子,就支持一下我这个干货writer吧,三连,三连,三连就是我最大的动力~,接下来会持续分享干货~(应该是k8s相关知识)。点赞👍 关注❤️ 分享👥