Disruptor是一个单机稳定的生产消费模型,虽然当前在分布式环境下已经不受重视,但是从设计思想和一些细节实现方面仍然值得借鉴。
在MAC开发机上,对比了一个简单的Disruptor模型和ArrayBlockingQueue、LinkedBlockingQueue
| 量级 | Disruptor | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|---|
| 5000 w | 5802ms | 8500ms | 11204ms |
后续会加入更多的队列进行对比。
可以看到确实在高吞吐量和低延迟的情况下表现性能会更好。比如交易系统、如千万级数据定时更新,读取数据库记录的主键列表,然后开始批量调用三方接口进行更新。此时数据量又不是很大,也没有必要使用大型消息中间件,如rabbitMwq,kafka等。此时采用disruptor内存队列不失为一个很好的方案。
Disruptor核心概念
-
环形缓冲区(Ring Buffer)
RingBuffer是扮演一个存储资源池的角色,提前预分配空间,避免频繁的gc,使用预加载填充,消除伪共享,以及2的幂次方容量方便运算。
ps:伪共享是多核计算中一个常见的问题,当多个线程尝试访问相同缓存行(Cache Line)上的不同变量时会发生。这会导致缓存行在多个核心之间频繁无效化和更新,从而降低性能。(这句话要怎么理解?)
--> 在MESI协议中,如果一个核心修改了一个缓存行,这个缓存行在其他核心中会被标记为无效(Invalid)。这意味着什么?想象一下,其他核心需要重新从主内存或者更高层级的缓存中重新读取这个数据,这会怎样影响性能?
--> 因此,伪共享导致的频繁缓存行失效和更新会频繁触发MESI协议,从而增加处理器之间的通信负担。
-
序列号(Sequence)
Sequence是序列号,你可以想象它作为一个指针,指向环形缓冲区中最后一个被该生产者写入或者消费者读取的事件的位置。当生产者发布一个新事件或者消费者处理一个事件后,它们的序列号会相应地增加。
在 Disruptor 框架中,处理消费者依赖关系时,不是所有消费者使用同一个
Sequence,而是每个消费者都有自己的Sequence,本身提供了类似AtomicLong的各个特性。生产者,消费者都是这个类的使用者,每个使用者都会维护一个Sequence来标识自己的读/写下标,disruptor里面大部分的并发代码都是通过对Sequence的值同步修改实现的,而非锁,消费者之间的协调和数据可见性是通过SequenceBarrier实现的,这是disruptor高性能的一个主要原因.ps:内存屏障:### SequenceBarrier 在java中,如果你对一个volatile字段进行写操作,你必须知道:
1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
-
事件(Event)
disruptor中传递的事件,相当于消息队列中放进去的元素。
-
事件处理器(Event Processors)
处理Event的循环,在循环中获取Disruptor的事件,然后把事件分配给各个handler
Disruptor核心思想
- 二阶段提交+cas操作替代重量级的锁
- 预创建对象,避免频繁的gc
- 缓存填充,避免缓存失效
- 位运算替代取模运算
Disruptor源码的几个关键部分
在create单生产者里,关注一下next方法
public long next(int n) {
if (n < 1) {
throw new IllegalArgumentException("n must be > 0");
} else {
long nextValue = this.nextValue; //这是生产者当前想要写入的起始序号
long nextSequence = nextValue + (long)n; //生产者想要占有的最后一个下标位置
long wrapPoint = nextSequence - (long)this.bufferSize;//预计算减去一个环的值
long cachedGatingSequence = this.cachedValue; //最慢消费者消费到的值
//如果这个值大于消费者到的值(说明跨度太大,根本没法提供)//
或者
判断的是最慢消费进度超过了我们即将要申请的sequence,乍一看这应该是不可能的吧,都还没申请到该sequence怎么可能消费到呢?找了些资料,发现确实是存在该场景的:`RingBuffer`提供了一个叫`resetTo`的方法,可以重置当前已申请sequence为一个指定值并publish出去:
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue) {
//插入一个StoreLoad屏障,防止是因为内存可见性导致的消费者消费不了数据
this.cursor.setVolatile(nextValue);
long minSequence;
while(wrapPoint > (minSequence = Util.getMinimumSequence(this.gatingSequences, nextValue))) {
//如果生产方 减去一个环值 和 当前消费和可用值的最小值进行比较,
// 通知下消费者
waitStrategy.signalAllWhenBlocking();
//这个parkNanos有点意思,对比Thread.sleep 他这种类型的方法通常用于高级并发控制,如实现自旋锁或其他忙等待机制时。在这个上下文中,它用于轻微地延迟线程,以减少对 CPU 的压力,同时等待某个条件变为真,如等待缓冲区中有可用空间。
LockSupport.parkNanos(1L);
}
如果到了,把消费者消费到的值,置为这个最小值
this.cachedValue = minSequence;
}
下一个可用位置真的可用了
this.nextValue = nextSequence;
return nextSequence;
}
}
和publish方法:这里就简单了,前面都做好了这里直接发布就可,一个简单的记录+唤醒因此而等待的消费者。
public void publish(long sequence) {
//
this.cursor.set(sequence);
this.waitStrategy.signalAllWhenBlocking();
}
sequenceBarrier.waitFor
// class ProcessingSequenceBarrier
public long waitFor(final long sequence)
throws AlertException, InterruptedException, TimeoutException
{
checkAlert();
// waitStrategy派上用场了,这是我们在构造Disruptor的时候的入参(也是构造RingBuffer的入参)
long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
// 理论上没有可能为true,因为当前每种waitStrategy内都保证了availableSequence一定大于等于sequence
if (availableSequence < sequence)
{
return availableSequence;
}
// 返回最大的已发布的sequence,在单生产者模式下这个函数返回值就等于availableSequence
return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}
waitStrategy是一种等待策略,见下文。
waitfor方法是消费者在等待生产者发布更多事件时的等待逻辑。用于控制消费者的进度,以确保它们不会超前于生产者或其他依赖的消费者
接收一个消费者目标位置,生产者当前生产位置,一个依赖序列(用于批量事件处理),和一个序列屏障。
public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier) throws AlertException, InterruptedException {
//检查是为了确定生产者是否已经发布了足够的事件,使得序列号达到或超过消费者请求处理的序列号 `sequence`。
if (cursorSequence.get() < sequence) {
//ReentrantLock();
this.lock.lock();
try {
// 当前生产者的生产位置小于当前目标位置
while(cursorSequence.get() < sequence) {
barrier.checkAlert();
// 消费者进入condition等待队列并让出锁
this.processorNotifyCondition.await();
}
} finally {
//
this.lock.unlock();
}
}
long availableSequence;
// 这是对其他依赖序列的检查。在某些情况下,一个消费者可能需要等待其他依赖消费者处理完事件。
while((availableSequence = dependentSequence.get()) < sequence) {
barrier.checkAlert();
//减轻自旋锁
ThreadHints.onSpinWait();
}
返回可
return availableSequence;
}
signalAllWhenBlocking():当生产者发布新事件并更新 cursorSequence 后,会调用这个方法,以通知等待的消费者。
public void signalAllWhenBlocking() {
this.lock.lock();
try {
this.processorNotifyCondition.signalAll();
} finally {
this.lock.unlock();
}
}
sequencer.getHighestPublishedSequence
从WaitStrategy.waitFor()返回后,得到的是RingBuffer上已申请进度sequence或者是依赖消费者消费进度sequence(当然如果把cursorSequence也看成一种依赖的话,理解起来就统一了)。注意一个形容词——“已申请”,而不是“已发布”,“已申请”意味着还不一定“已发布”,也就是还不能消费。所以,SequenceBarrier.waitFor最后还有一步sequencer.getHighestPublishedSequence(sequence, availableSequence)。
当然如果你很仔细的看到这里并且对于前面的内容都理解了,你可能会产生疑问:对于单生产者来说,本来就是在publish的时候才更新cursor的啊?那上一步从WaitStrategy.waitFor()获取到的不就是“已发布”的进度sequence吗?是的,你说得很正确。对于单生产者确实如此,所以但生产者对应的实现为:
public long getHighestPublishedSequence(long lowerBound, long availableSequence)
{
return availableSequence;
}
那么消费线程的具体逻辑是?看看BatchEventProcessor的run()方法:
public void run()
{
if (!running.compareAndSet(false, true))
{
throw new IllegalStateException("Thread is already running");
}
sequenceBarrier.clearAlert();
notifyStart();
T event = null;
// 成员变量sequence维护该Processor的消费进度
long nextSequence = sequence.get() + 1L;
try
{
while (true)
{
try
{
// 以nextSequence作为底线,去获取最大的可用sequence(也就是已经被publish的sequence)
final long availableSequence = sequenceBarrier.waitFor(nextSequence);
// 如果获取到的sequence大于等于nextSequence,说明有可以消费的event,从nextSequence(包含)到availableSequence(包含)这一段的事件就作为同一个批次
while (nextSequence <= availableSequence)
{
event = dataProvider.get(nextSequence);
// 调用了前面注册的回调函数
eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
nextSequence++;
}
// 消费完一批之后 一次性更新消费进度
sequence.set(availableSequence);
}
catch (final TimeoutException e)
{
// waitFor超时的场景
notifyTimeout(sequence.get());
}
catch (final AlertException ex)
{
if (!running.get())
{
break;
}
}
catch (final Throwable ex)
{
// 消费过程中如果抛出异常,表面上看会更新消费进度,也就是说没有补偿机制。但实际上默认的策略是会抛异常的,消费线程会直接结束掉
exceptionHandler.handleEventException(ex, nextSequence, event);
sequence.set(nextSequence);
nextSequence++;
}
}
}
finally
{
notifyShutdown();
running.set(false);
}
}
补充的背景知识:
现代的处理器要比同代的内存快得多的多。为了填补这样一个速度差距的鸿沟,CPU使用了复杂的缓存系统——非常快的通过硬件实现的无链哈希表。这些缓存系统通过消息传递协议与其他处理器CPU的缓存系统保持协调一致。另外作为补充,处理器的“存储缓冲”可以将写操作从上述缓冲上卸载下来,在一个写操作将要发生的时候,缓存协调协议通过这样的一个“失效队列”快速的通知失效消息。
这些对于数据来说意味着,当某个数据值的最后一个版本刚刚被写操作执行之后,将会被存储登记给一个存储缓冲——可能是CPU的某一层缓存、或者是一块内存区域。如果线程想要共享这一数据,那么它需要以一种有序的方式轮流对其他线程可见,这是通过处理器协调消息的协调来实现的。这些及时的协调消息的生成,是又内存栅栏来控制的。
读操作内存栅栏对CPU的加载指令进行排序,通过失效队列来得知当前缓存的改变。这使得读操作内存栅栏对其之前的已排序的写操作有了一个持久化的视界。
写操作内存栅栏对CPU的存储指令进行排序,通过存储缓冲执行,因此,通过对应的CPU缓存来刷新写输出。写操作内存栅栏提供了一个在其之前的存储操作如何发生的、有序的视界。
一个完整的内存栅栏即对加载排序也对存储排序,但这只针对执行该栅栏的CPU。
一些CPU还有上述三种元件的变体,但是介绍这三种元件已经足够来理解相关的复杂联系了。在Java的内存模型中,对一个volatile类型成员变量的域的读和写,分别实现了读内存栅栏和写内存栅栏。