Disruptor之理论基础


Disruptor之理论基础

disruptor的设计理念主要围绕以下几个核心思想:无锁设计,缓存行填充,序列号机制,事件驱动,高吞吐量;

传统队列的一些问题

  • 锁性能瓶颈
    锁提供了一种互斥机制,通过锁可以保证数据安全性,但是锁的使用会带来性能瓶颈,根据Disruptor官网的说法使用锁的成本如下

性能分析
可以看到性能如下:无锁 > valatile > CAS > lock,其中可以看到即使是CAS的性能也远远大于lock的方式,这是因为CAS不需要切换到内核态,而lock需要频繁的切换到内核态,这会带来很大的性能开销;

这里有一个知识点,对于互斥锁或者信号量发生竞争时,都会使用内核仲裁来决定锁的归属,从用户态切换到内核态会涉及到切换上线文,上线文切换会发生缓存行失效,这两个开销加起来会导致锁的性能大幅下降;

紧接着就引出第二点,缓存行失效的问题;

  • 缓存行

缓存行指的是现代的CPU为了提高内存访问效率,在加载内存时是通过加载一个缓存行,通常缓存行的大小为64字节;
在CPU进行线程切换时,因为不同的线程可能会访问不同的内存地址,当线程切换时,CPU需要重新加载新的缓存行,这会导致之前的缓存行失效,从而带来性能下降,这也就是缓存行失效问题;
同一个缓存行中可能同时放着两个不同的对象,当线程修改某一个对象时,会导致整个缓存行失效,让CPU重新对整个缓存行进行加载,从而影响另一个对象的访问性能,这就是缓存行伪共享问题,可以通过增大数组元素中的间隔来解决;

Disruptor的设计理念

  • 环形数据结构
    Disruptor使用环形缓冲区(RingBuffer)作为数据存储结构,在初始化时预先分配内存,通过使用环形缓冲区可以避免频繁的内存分配和垃圾回收,从而提高性能,其中环形缓冲区只是逻辑上采用的,实际上是通过数组实现的,如图第二部分所示,当发布到第17个事件时,实际上是覆盖了第1个事件的位置;
    采用连续内存的数组存储数据的好处是相邻的数据会被加载到相同的CPU cache中,所以访问遍历的速度会很快,而传统的链表结构会导致数据分散在内存中,从而影响缓存命中率;

RingBuffer结构

  • 无锁设计
    Disruptor通过使用无锁设计来避免锁竞争带来的性能瓶颈,它使用了CAS操作和自旋锁等技术来实现线程间的协调,从而避免了锁的使用;

  • 序列号机制
    Disruptor通过序列号机制来实现生产者和消费者之间的协调,每个事件都有一个唯一的序列号,生产者在发布事件时会递增序列号,消费者在消费事件时也会递增序列号,通过比较序列号来判断事件是否已经被消费;

  • 缓存行处理
    Disruptor通过缓存行填充技术来避免缓存行伪共享问题,它在数据结构中添加填充字段,确保不同的对象不会共享同一个缓存行,从而提高缓存命中率;

  • 事件驱动模型
    Disruptor采用事件驱动模型,生产者发布事件后,消费者会被通知进行消费,这种模型可以减少轮询等待的开销,提高系统的响应速度;

Disruptor的实现

Disruptor核心类图

核心数据结构:RingBuffer

RingBuffer是由数组\事件\缓冲区大小\生产者序列号(Sequencer)\消费者序列号(Sequences)组成,各自功能如下:

  • 数组:一个用于存储事件对象且大小固定的数组
  • 事件:RingBuffer中存储的数据单元,在RingBuffer初始化时通过事件工厂预创建
  • 缓冲区大小:RingBuffer的大小,必须是2的N次方,以便于通过位运算进行索引计算
  • 生产者序列号(Sequencer):用于跟踪生产者发布事件的位置
  • 消费者序列号(Sequences):用于跟踪消费者消费事件的位置

等待策略

等待策略分为消费等待策略和生产等待策略,主要用于控制生产者和消费者在等待事件时的行为

  • 生产等待策略(ClaimStrategy)
    确保多个生产者在并发写入时,能够安全、无冲突地获取到独占的 RingBuffer 序列号,有两种策略:

  • 单生产者策略(SingleThreadedClaimStrategy):适用于单个生产者的

  • 多生产者策略(MultiThreadedClaimStrategy):适用于多个生产者的

  • 消费者等待策略(WaitStrategy)
    优化消费者等待新事件时的 CPU 使用效率和延迟之间的权衡,有以下几种策略:

类型 策略名称 核心机制 延迟 (Latency) CPU 占用 适用场景
I. 极限性能型 1. BusySpinWaitStrategy 纯自旋空循环,不放弃 CPU。 最低 (Low) 极高 (Max) 对延迟有毫秒级甚至纳秒级的苛刻要求,且生产者持续高产,不计 CPU 消耗。
II. 平衡优化型 2. YieldingWaitStrategy 短暂自旋后,调用 Thread.yield() 放弃时间片。 低 (Low) 中等 (Medium) 延迟敏感但希望适度节省 CPU 的场景,是最常用的平衡策略。
3. SleepingWaitStrategy 自旋后,周期性地调用 Thread.sleep(1),让出 CPU 并休眠。 中等 (Moderate) 低 (Low) 适用于延迟要求不高,但需要更彻底节省 CPU 的多生产者场景。
III. 资源友好型 4. BlockingWaitStrategy 使用 Java 锁和条件变量阻塞线程,被生产者唤醒。 中高 (Moderate/High) 最低 (Min) 对延迟不敏感,追求最低 CPU 占用的场景,如日志或监控系统。
5. LiteBlockingWaitStrategy 使用 LockSupport.parkNanos 进行轻量级阻塞。 中高 (Moderate/High) 最低 (Min) 类似于 Blocking,但试图减少与操作系统锁的交互开销,略微更高效。
IV. 高级自适应型 6. PhasedBackoffWaitStrategy 三阶段混合:忙等 -> Yield -> 阻塞/定时阻塞。 自适应 自适应 生产者吞吐量极不稳定的场景。高负载时低延迟,空闲时低 CPU。
7. TimeoutBlockingWaitStrategy 阻塞等待,但设有超时时间。 中高 (Moderate/High) 最低 (Min) 需定期处理逻辑(如心跳、超时检查)的消费者,不希望永远阻塞。
8. LiteTimeoutBlockingWaitStrategy 轻量阻塞等待,设有超时时间。 中高 (Moderate/High) 最低 (Min) LiteBlocking 的超时版本,结合了轻量阻塞和周期性唤醒的需求。

写入逻辑

单生产者写入流程

步骤 1: 申请下一个订单流水号 (Claim)
Disruptor 单生产者:唯一的生产者直接操作自己的序列号,进行一次原子递增操作
步骤 2: 检查RingBuffer是否已满
通过比较下一个序列号和消费者的最小序列号加上缓冲区大小来判断RingBuffer是否已满,如果已满则需要等待消费者消费;
步骤 3: 替换事件
将数据写入该位置的事件对象中;
步骤 4: 发布事件
通过更新生产者序列号来发布事件,通知消费者有新事件可以消费;

多生产者写入流程

步骤 1: 抢夺下一个订单流水号 (Claim with CAS)
所有生产者同时尝试读取当前的最高序列号(例如 9),并尝试使用 CAS (Compare-and-Swap) 将其原子性地更新到 10。更新成功的生产者获得了序列号 10,可以10的位置进行写入;更新失败的生产者需要重新读取最新的最高序列号并重试。
步骤 2: 检查RingBuffer是否已满
与单生产者类似,通过比较下一个序列号和消费者的最小序列号加上缓冲区大小来判断RingBuffer是否已满,如果已满则需要等待消费者消费;
步骤 3: 替换事件
将数据写入该位置的事件对象中;
步骤 4: 按序发布事件 (Gated Publish)
多生产者需要确保事件按序发布,即使生产者抢到了序列号10,也不能直接发布,而是需要等待序列号9被发布后才能发布序列号10,通过这种方式确保事件的顺序性;

简单来说就是:多生产者模式下通过CAS的方式竞争下一个下入位置,更新成功的生产者向持有的那个位置进行写入,同时发布事件时必须顺序的进行发布

消费逻辑

单个消费者消费流程

步骤 1: 检测是否有新数据
通过比较消费者的序列号和生产者的序列号来判断是否有新数据可供消费;
步骤 2: 决策是消费还是等待
如果有新数据则继续消费,否则根据等待策略进行等待;
步骤 3: 批量获取待消费事件范围
为了提高效率,消费者可以批量获取一段范围内的事件进行消费;
步骤 4: 处理事件
消费者对获取到的事件进行处理;

多个消费者消费流程

多个消费者的处理流程比较复杂,因为消息处理策略可以分为广播消息和事件消息两种,并且在消费上也可以分为独立消费和协同消费两种模式,所以多消费者的处理流程需要根据具体的场景来设计;

参考资料

Disruptor官网
Disruptor 高性能队列原理浅析


  TOC