Disruptor介绍及原理讲解

简介

Disruptor是一个无锁有界内存队列开源框架,最大的特点就是性能非常高高高。很多知名项目都有用到它比如我们熟悉的Log4j 2 .

本文主要介绍它如何做到高性能,以及具体的框架设计。

为什么性能这么强大?

主要是因为有这三个法宝:RingBuffer,无锁设计和缓存行填充。

RingBuffer

Disruptor底层采用RingBuffer的结构,RingBuffer大家都知道,就是一个循环使用下标的数组嘛。

image.png

计算访问下标的时候,通过取余计算 (cursor % size )来得到数组下标。(一个trick是,当size是2的幂的时候,可以用 cursor & (size - 1) 来快速计算下标。所以Disruptor指定size必须是2的幂。)

用RingBuffer的好处:

  1. 不需要清数据,用新数据去覆盖旧数据,减少GC
  2. 底层是数组,充分利用缓存

无锁设计

怎么做到的?

在Disruptor中,生产者和消费者有各自的游标,用来指导需要写入或读取的位置。

消费者对节点的唯一操作是读而不是写,因此不需要加锁。

只有一个生产者的时候,只需要保证生产者的游标不会超过最慢的消费者一圈(即,不会把消费者还没读完的数据覆盖掉)即可,因此不需要锁。

当有多个生产者时,Disruptor采用CAS来保证游标的线程安全。在整个复杂的框架中,只有这一个地方出现多线程竞争修改同一个变量值。具体的交互在后面讲。

(另一方面,用volatile来标记游标,采用内存屏障来代替锁?)

缓存行填充

先来了解一个概念,“伪共享”。

image.png

我们知道计算机有多级的缓存体系,越靠近CPU的缓存,速度越快,容量越小。而缓存由很多cache line组成,每个cache line通常是64bytes,所以一个cache line通常可以缓存8个long变量。从内存中拉取数据的时候,会把相邻的数据都一起加载到缓存中。在某些情况下这个缓存行优势会失效,导致并发速度反而下降了,这种情况称为伪共享。以下是一个典型的例子:

假设有两个线程Thread1和Thread2,分别在Core1和Core2上运行。有两个变量head和tail由它们共享,Thread1只读写head, Thread2只读写tail。理想情况下它们不应该有干扰,但是我们可以看到,当Thread1写入head以后,其他Core对应的cache line被都置为失效,也就意味着Core2想要读写tail,需要从内存中重新读取,而这显然是一种浪费。

image

我们可以通过缓存行填充来解决这类问题:

class LhsPadding
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}
 
class Value extends LhsPadding
{
    protected volatile long value;
}
 
class RhsPadding extends Value
{
    protected long p9, p10, p11, p12, p13, p14, p15;
}

在Disruptor中,游标做了类似的处理。

在由我们自己定义的Event类中,也值得这样做。如果有不同的生产者往不同的字段写入(?),我们要确保各个字段之间不会出现伪共享。

具体设计

直觉上理解一下Disruptor的设计:Disruptor通过对游标的管理,保证任何时候只有一个生产者去写一个槽,就省了很多并发问题;只要好好看好游标就行了。

消费

先从比较好理解的消费者讲起。

7

在Disruptor中,消费者被称为EventProcessor,通过SequenceBarrier和RingBuffer交互。

如上图Stage2所示,事件处理器的最大序号是16.它向SequenceBarrier调用waitFor(17)以获得17格中的数据。如果没有数据写入RingBuffer,Stage2事件处理器将挂起等待下一个序号。但是,如上图所示的情况,RingBuffer已经被填充到18格,所以waitFor函数将返回18并通知事件处理器,它可以读取包括直到18格在内的数据,如下图所示。

6

你应该已经感觉得到,这样做是怎样有助于平缓延迟的峰值了——以前需要逐个节点地询问“我可以拿下一个数据吗?现在可以了么?现在呢?”,消费者现在只需要简单的说“当你拿到的数字比我这个要大的时候请告诉我”,函数返回值会告诉它有多少个新的节点可以读取数据了。因为这些新的节点的确已经写入了数据(Ring Buffer本身的序号已经更新),而且消费者对这些节点的唯一操作是读而不是写,因此访问不用加锁。这太好了,不仅代码实现起来可以更加安全和简单,而且不用加锁使得速度更快。

另一个好处是——你可以用多个消费者去读同一个RingBuffer ,不需要加锁,也不需要用另外的队列来协调不同的线程(消费者)。这样你可以在Disruptor的协调下实现真正的并发数据处理。

生产

向RingBuffer写入数据需要经过两阶段提交。

只有一个发布者时

首先,发布者必须确定RingBuffer中下一个可以插入的格, RingBuffer持有最近写入格的序号(下图中的18格),从而确定下一个插入格的序号。RingBuffer通过检查所有EventProcessor正在从RingBuffer中读取的当前序号来判断下一个插入格是否空闲,只需要保证下一个插入格已经被所有EventProcessor读取过即可。

3

发现了下一个插入格:

4

当发布者得到下一个序号后,它可以获得该格中的对象,并可以对该对象进行任意操作。你可以把格想象成一个简单的可以写入任意值的容器。

同时,在发布者处理19格数据的时候,RingBuffer的序号依然是18,所以其他事件处理器将不会读到19格中的数据。

对象的改动保存进了RingBuffer:

5

最终,发布者将数据写入19格后,通知RingBuffer发布19格的数据。这时,RingBuffer更新序号并且所有从RingBuffer读数据的事件处理器都可以看到19格中的数据。

总结一下,发布者需要先申请一个可写入的位置,然后再写入然后提交,这是一个明显的两阶段提交设计。

有多个发布者时

单生产者的情况比较简单,当有多个生产者时,申请写入位置的时候就会产生竞争。上文说过,Disruptor采用CAS来保证游标的线程安全。直接上源码吧。这段源码应该是最能体现Disruptor核心设计思想的部分了。

com.lmax.disruptor.MultiProducerSequencer

/**
 * @see Sequencer#next(int)
 */
@Override
public long next(int n) //生产者申请分配n个位置
{
    if (n < 1 || n > bufferSize)
    {
        throw new IllegalArgumentException("n must be > 0 and < bufferSize");
    }
 
    long current;
    long next;
 
    do
    {
        current = cursor.get(); // cursor是已经分配的位置的头
        next = current + n; // 待分配位置的头
 
        long wrapPoint = next - bufferSize; // 待分配位置回退一圈的位置。用于和消费者的位置比较,避免把还没有读的位置也分配了
        long cachedGatingSequence = gatingSequenceCache.get(); // 缓存的最慢消费者的位置
 
        if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) // 如果缓存的最慢消费者的位置不符合条件,则重新进入检查
        {
            long gatingSequence = Util.getMinimumSequence(gatingSequences, current);
 
            if (wrapPoint > gatingSequence) // 最慢消费者的位置太小,会被生产者覆盖,因此不能完成分配,所以让出CPU。
            {
                LockSupport.parkNanos(1);
                continue;
            }
 
            gatingSequenceCache.set(gatingSequence);
        }
        else if (cursor.compareAndSet(current, next))  // 设置新的游标位置! 由于CAS的特性,多个生产者同时试图修改current游标的时候,只有一个会成功
                                                        // 其他的会重新进入循环,获取新的游标位置继续尝试申请。
        {
            break;
        }
    }
    while (true);
 
    return next;
}

进阶

Disruptor Wizard
提供了一系列API来设置event handlers,并设置它们之间的依赖关系。

WaitStrategy
当消费者等待在SequenceBarrier上时,有许多可选的等待策略,不同的等待策略在延迟和CPU资源的占用上有所不同,可以视应用场景选择。

源码梳理

3.x的代码,感兴趣的同学可以参考: https://zhanghaoxin.blog.csdn.net/article/category/6121943 ,对源码结构有较详细的讲解。或者参考 Disruptor 3.x源码梳理(简版)

参考资料

http://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf 论文

https://tech.meituan.com/2016/11/18/disruptor.html by美团技术团队

http://ifeve.com/disruptor/ 官网发布的系列文章的译文(比较老了,很多是根据1.x和2.x的讲解的,但是用来参考核心思想是没问题的)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容