数据结构与算法-9 高性能循环队列 Disruptor
目录
54 | 高性能队列 Disruptor 背后的数据结构和算法
Disruptor 是一种内存消息队列
,类似 Kafka。不过,和 Kafka 不同的是,Disruptor 是线程之间
用于消息传递的队列。它在 Apache Storm、Camel、Log4j 等很多知名项目中都有广泛应用。
之所以如此受青睐,主要还是因为它的性能表现非常优秀。它比 Java 中另外一个非常常用的内存消息队列 ArrayBlockingQueue
(ABS)的性能要高一个数量级,可以算得上是最快的内存消息队列了。它还因此获得过 Oracle 官方的 Duke
大奖。
Disruptor 是如何做到如此高性能的?其底层依赖了哪些数据结构和算法?
生产者-消费者模型
“生产者 - 消费者模型”大部分人都知道。在这个模型中,“生产者”生产数据,并且将数据放到一个中心存储容器中。之后,“消费者”从中心存储容器中,取出数据消费。
那么,这个模型里面存储数据的中心存储容器,是用什么数据结构来实现的呢?
实际上,实现中心存储容器最常用的一种数据结构,就是队列。队列支持数据的先进先出。正是这个特性,使得数据被消费的顺序性可以得到保证,也就是说,早被生产的数据就会早被消费。
队列有两种实现思路,一种是基于链表实现的链式队列
,另一种是基于数组实现的顺序队列
。不同的需求背景下,我们会选择不同的实现方式。
- 如果我们要实现一个
无界队列
,也就是说,队列的大小事先不确定,理论上可以支持无限大。这种情况下,我们适合选用链表来实现队列。因为链表支持快速地动态扩容。 - 如果我们要实现一个
有界队列
,也就是说,队列的大小事先确定,当队列中数据满了之后,生产者就需要等待。直到消费者消费了数据,队列有空闲位置的时候,生产者才能将数据放入。
实际上,相较于无界队列,有界队列的应用场景更加广泛。毕竟,我们的机器内存是有限的。而无界队列占用的内存数量是不可控的。对于实际的软件开发来说,这种不可控的因素,就会有潜在的风险。在某些极端情况下,无界队列就有可能因为内存持续增长,而导致 OOM
。
基于循环队列的模型
由于非循环的顺序队列在添加、删除数据的工程中,会涉及数据的搬移操作,导致性能变差。而循环队列正好可以解决这个数据搬移的问题,所以,大部分用到顺序队列的场景中,我们都选择用顺序队列中的循环队列。
实际上,循环队列这种数据结构,就是我们今天要讲的内存消息队列的雏形。我借助循环队列,实现了一个最简单的“生产者 - 消费者模型”
public class Queue {
private Long[] data;
private int size = 0, head = 0, tail = 0;
public Queue(int size) {
this.data = new Long[size];
this.size = size;
}
public boolean add(Long element) {
if ((tail + 1) % size == head) return false; //队列满了
data[tail] = element;
tail = (tail + 1) % size;
return true;
}
public Long poll() {
if (head == tail) return null; //队列空了
long ret = data[head];
head = (head + 1) % size;
return ret;
}
}
public class Producer {
private Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
public void produce(Long data) throws InterruptedException {
while (!queue.add(data)) {
Thread.sleep(100); //当队列满了之后,生产者就轮训等待
}
}
}
public class Consumer {
private Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
public void comsume() throws InterruptedException {
while (true) {
Long data = queue.poll();
if (data == null) {
Thread.sleep(100); //当队列空了之后,消费者就轮训等待
} else {
// ...
}
}
}
}
基于加锁的并发模型
如果我们只有一个生产者往队列中写数据,一个消费者从队列中读取数据,上面的代码是没有问题的。但是,如果有多个生产者在并发地往队列中写入数据,或者多个消费者并发地从队列中消费数据,那上面的代码就不能正确工作了。
在多个生产者或者多个消费者并发
操作队列的情况下,刚刚的代码主要会有下面两个问题:
- 多个生产者写入的数据可能会互相覆盖
- 多个消费者可能会读取重复的数据
public boolean add(Long element) {
if ((tail + 1) % size == head) return false;
data[tail] = element;
tail = (tail + 1) % size;
return true;
}
从这段代码中,我们可以看到,第 3 行给 data[tail]
赋值,然后第 4 行才给 tail 的值加一。赋值和 tail 加一两个操作,并非原子操作。这就会导致这样的情况发生:
- 当线程 1 和线程 2 同时执行 add() 函数的时候,线程 1 先执行完了第 3 行语句,将
data[7]
(tail 等于 7)的值设置为 12 - 在线程 1 还未执行到第 4 行语句之前,也就是还未将 tail 加一之前,线程 2 执行了第 3 行语句,又将
data[7]
的值设置为 15,也就是说,那线程 2 插入的数据覆盖了线程 1 插入的数据 - 原本应该插入两个数据(12 和 15)的,现在只插入了一个数据(15)。
那如何解决这种线程并发往队列中添加数据时,导致的数据覆盖、运行不正确问题呢?
最简单的处理方法就是给这段代码加锁
,同一时间只允许一个线程执行 add() 函数。这就相当于将这段代码的执行,由并行改成了串行
,也就不存在我们刚刚说的问题了。
不过,天下没有免费的午餐,加锁将并行改成串行,必然导致多个生产者同时生产数据的时候,执行效率的下降。当然,我们可以继续优化代码,用CAS
(compare and swap,比较并交换)操作等减少加锁的粒度,但是,这不是我们这节的重点。我们直接看 Disruptor 的处理方法。
基于无锁的并发模型
之前的实现思路中,队列只支持两个操作,添加数据和读取并移除数据,分别对应代码中的 add() 函数和 poll() 函数,而 Disruptor 采用了另一种实现思路。
对于生产者来说,它往队列中添加数据之前,先申请可用空闲存储单元,并且是批量地申请连续的 n 个存储单元。当申请到这组连续的存储单元之后,后续往队列中添加元素就可以不用加锁了,因为这组存储单元是这个线程独享
的。当然,申请存储单元的过程也是需要加锁的。
对于消费者来说,处理的过程跟生产者是类似的。它先申请一批连续可读的存储单元(这个申请的过程也是需要加锁的),当申请到这批存储单元之后,后续的读取操作就可以不用加锁了。
不过,Disruptor 存在一个弊端:
- 假设生产者 A 申请到了下标为 3 到 6 的存储单元
- 紧跟着生产者 B 申请到了下标是 7 到 9 的存储单元
- 那么,在 3 到 6 没有完全写入数据之前,7 到 9 的数据是无法读取的
尝试解释:由于消费者需要顺序读数据,并且消费者
A*
申请到的存储单元,可能和生产者 A 申请到的存储单元不对应,所以如果前面的数据没写完就允许读后面的数据的话,可能出错
实际上,Disruptor 采用的是 RingBuffer
和 AvailableBuffer
这两个结构来实现我刚刚讲的功能。不过,因为我们主要聚焦在数据结构和算法上,所以我对这两种结构做了简化,但是基本思想是一致的。如果你对 Disruptor 感兴趣,可以去阅读一下它的源码。
总结引申
常见的内存队列往往采用循环队列
来实现。这种实现方法,对于只有一个生产者和一个消费者的场景,已经足够了。但是,当存在多个生产者或者多个消费者的时候,单纯的循环队列的实现方式,就无法正确工作了。
这主要是因为,多个生产者在同时往队列中写入数据的时候,在某些情况下,会存在数据覆盖的问题。而多个消费者同时消费数据,在某些情况下,会存在消费重复数据的问题。
针对这个问题,最简单、暴力的解决方法就是,对写入和读取过程加锁
。这种处理方法,相当于将原来可以并行执行的操作,强制串行执行,相应地就会导致操作性能的下降。
为了在保证逻辑正确的前提下,尽可能地提高队列在并发情况下的性能,Disruptor 采用了两阶段写入
的方法。
- 在写入数据之前,先加锁申请批量的空闲存储单元,之后往队列中写入数据的操作就不需要加锁了,写入的性能因此就提高了
- 在读取数据之前,先加锁申请批量的可读取的存储单元,之后从队列中读取数据的操作也就不需要加锁了,读取的性能因此也就提高了
你可能会觉得这个优化思路非常简单。实际上,不管架构设计还是产品设计,往往越简单的设计思路,越能更好地解决问题。正所谓“大道至简”,就是这个意思。
2017-07-29
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7256922.html