中文参考:https://lixiangyun.gitbook.io/disruptor/

Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,是一个支持生产者 / 消费者模式环形队列,能够在无锁的条件下进行并行消费,也可以根据消费者之间的依赖关系进行先后消费次序。

依赖:

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

# 设计方案

传统的阻塞队列的并发瓶颈在于队列两端,通过上锁保证并发安全,但是这也会导致性能的下降。一个队列的设计一般有三个必要的变量:

  • 头指针
  • 尾指针
  • 队列当前大小

这三个变量总是会有很多竞争,而且这三个变量常常在一个 cache line(通常为 64 字节)中,很多时候会导致伪共享

假设有两个 CPU:A 和 B,A 充当生产者,B 充当消费者。也就是 A 会修改头指针,B 会修改尾指针,这两个变量因为都比较小,所以会出现在同一个缓存行,当 A 和 B 都将对应的缓存行加载到自己的 cache 后,A 修改了缓存行中的头指针,于是该缓存行失效,同时会通知 B,这个缓存行已经失效了,你需要重新去读取。

关于伪共享,这篇文章可以帮到你:https://tech.meituan.com/2016/11/18/disruptor.html

  • 环形数组:避免垃圾回收,同时数组对处理器的缓存机制友好
  • 元素位置定位:长度 2^n,通过位运算加快定位速度,下标递增,并且 index 为 long,即使 100 万 QPS(请求数 / 秒),也要 30 万年才能用完
  • 无锁设计:每个生产者或者消费者线程通过先申请可以操作的元素在数组中的位置,直接进行写入或者读取数据
  • 解决伪共享

# RingBuffer

它是一个环(首尾相接的环),你可以把它用做在不同上下文(线程)间传递数据的 buffer。ringbuffer 拥有一个序号,这个序号指向数组中下一个可用的元素。

序号映射到下标需要取模: idx = seq % len 。因为 ringbuffer 的长度是 2^n ,所以取模又可以使用位运算: idx = seq & (len - 1)

RingBuffer 维护两个指针: cursornext

cursor 指向最后一个填充了数据的区块, next 指向第一个未填充的区块。

# 读取数据

假设现在通过一些 “魔法” 已经把数据写入到 Ring Buffer 了,怎样从 Ring Buffer 读出这些数据呢?

消费者是一个线程,它可以访问 ConsumerBarrier 对象,该对象由 RingBuffer 创建并且代表消费者与 RingBuffer 进行交互。

消费者可以调用 ConsumerBarrier 对象的 waitFor() 方法,传递它所需要的下一个序号:

final long availableSeq = consumerBarrier.waitFor(nextSequence);

ConsumerBarrier 自身也维护了一个 cursor 作为本地标识,只要队列可以提供比 cursor 大的序号,消费者就可以进行消费。

# 写入数据

RingBuffer 同样为生产者提供了一个 ProducerBarrier 对象进行交互。在后台,由 ProducerBarrier 负责所有的交互细节来从 RingBuffer 中找到下一个节点,然后才允许生产者向它写入数据。

为了不造成重叠(生产者覆盖掉还未消费的数据), ConsumerTrackingProducerBarrier 对象拥有所有正在访问 RingBuffer 的 消费者列表,如果我们想确定我们没有让 RingBuffer 重叠,需要检查所有的消费者们都读到了哪里。

如果生产者想要写入的序号占据的节点,消费者正在消费,那么就会自旋等待。

生产者的写入是两阶段提交:

  • 先申请可以写入的节点,拿到节点后就可以写入数据了
  • 等待 RingBuffer 的游标 cursor 追到当前位置,然后写入。(如果只有单生产者,这其实无所谓)

接下来 ProducerBarrier 会提醒消费者 buffer 有新的消息进入。本质上,自旋竞争是抢占序号,并不是抢占写入过程,多个生产者的情况下,是由 ClaimStrategy 分配下一个可用节点,比如 A 拿到了 3,B 拿到了 4,在 A 还没写入时,说不定 RingBuffer 的游标都已经走到了 5。

所以看起来, cursornext 指针可能就变成了这样:

ClaimSequence 不但负责分发序号,而且负责跟踪哪些序号已经被分配。

当 B 想要提交时,如果 A 还没有提交,那么游标肯定还没有到 B 这,所以 B 就会自旋等待(具体如何还得看 WaitStrategy )。

最近的 RingBuffer 版本去掉了 Producer Barrier。如果在你看的代码里找不到 ProducerBarrier,那就假设当我讲 “Producer Barrier” 时,我的意思是 “Ring Buffer”。

# LMAX 架构

原文地址:http://martinfowler.com/articles/lmax.html 作者:Martin Fowler

译文地址:http://www.jdon.com/42452 译者:banq

LMAX 是一种新型零售金融交易平台,它能够以很低的延迟 (latency) 产生大量交易 (吞吐量). 这个系统是建立在 JVM 平台上,核心是一个业务逻辑处理器,它能够在一个线程里每秒处理 6 百万订单。业务逻辑处理器完全是运行在内存中 (in-memory),使用事件源驱动方式 (event sourcing). 业务逻辑处理器的核心是 Disruptors,这是一个并发组件,能够在无锁的情况下实现网络的 Queue 并发操作。他们的研究表明,现在的所谓高性能研究方向似乎和现代 CPU 设计是相左的。

请注意,该部分是在讲解 LMAX,而不仅仅是 disruptor

LMAX 大致分为三部分:

  • 业务逻辑处理器 bussiness logic processor
  • 输入 input disruptor
  • 输出 output disruptor

# 业务逻辑处理

业务逻辑处理器依次取出消息并运行,产生输出事件,整个操作都是单线程、基于内存的操作,没有数据库或其他持久存储。并且没有对象 -- 关系数据库的映射,所有代码都是使用 Java 对象模型。

基于内存的模型还有一个重要问题:断电了该怎么办?事件概念是问题解决得和性能,业务逻辑处理器状态是有输入事件驱动的,只要这些 hi 见被持久化保存起来,那么就可以复盘,获取到当前状态。

当然,LMAX 还做了很多风控,比如在晚上系统不繁忙的时候构建快照,重新启动商业逻辑处理器的速度很快,一个完整的重新启动 – 包括重新启动 JVM 加载最近的快照,和重放一天事件 – 不到一分钟。

但是还不够快,所以就保持多个业务逻辑处理器同时运行,每个输入事件由多个处理器处理,只有一个输出有效,其他的忽略,如果一个处理器失败,切换到另一个,这种故障转移失败恢复是事件源驱动 (Event Sourcing) 的另外一个好处。

# DIsruptor 输出和输出

尽管业务逻辑是单线程中实现,但是调用业务对象方法前,需要将原始输入来自于消息形式,这个消息需要恢复成业务逻辑处理器能够处理的形式。事件源 Event Sourcing 依赖于让所有输入事件持久化,这样每个输入消息需要能够存储到持久化介质上,最后整个架构还有赖于业务逻辑处理器的集群。同样在输出一边,输出事件也需要进行转换以便能够在网络上传输。

如图复制和日志是比较慢的。所有业务逻辑处理器避免最任何 IO 处理,所有这些任务都应该相对独立,他们需要在业务逻辑处理器处理之前完成,它们可以以任何次序方式完成,这不同意业务逻辑处理器需要根据交易自然先后进行交易,这些都是需要的并发机制。所以就出现了 DIsruptor

# 改动

一个成熟的框架少不了迭代,后来甚至很多术语都改了:

  • Costomer -> EventProcessor (事件处理器)和 EventHandler (事件句柄)
  • ProducerBarrier (生产者屏障)合并到了 RingBuffer
  • Entry -> Event