Disruptor深度解析-RingBuffer

前言

RingBuffer是Disruptor框架负责数据存储的模块,大部分文章也将其称之为环形缓存区,本文将对其实现原理进行深度探究。

本文依赖Disruptor版本为3.4.3

继承体系

RingBuffer继承体系类图

实现接口说明

  • DataProvider:获取指定位置的数据;
  • EventSink:发布事件到RingBuffer的接口定义,提供了各种事件发布的操作方法;
  • Cursored:获取当前游标位置;在Disruptor中都是依赖于Sequence的value;
  • Sequenced:提供了RingBuffer相关的sequence判断操作方法,包括获取大小、占用slot、发布等方法;

继承类说明

  • RingBufferFields:实现RingBuffer内存预分配以及数据定位等功能,此类非常关键,后续进行详细介绍;
  • RingBufferPad:解决缓存行伪共享的填充类;

RingBuffer初始化流程

//  RingBuffer的构造方法
RingBuffer(EventFactory<E> eventFactory, Sequencer sequencer) {
        // 调用父类即RingBufferFields的构造方法
        super(eventFactory, sequencer);
}

可以看出RingBuffer的初始化流程逻辑均在其父类中,我们详细分析RingBufferFields类的实现逻辑;

abstract class RingBufferFields<E> extends RingBufferPad {
    // 用来预留填充的单侧slot个数,加速计算
    private static final int    BUFFER_PAD;
    // RingBuffer中首个slot的偏移位置
    private static final long   REF_ARRAY_BASE;
    // log2(scale),该值与BUFFER_PAD作用相似,主要用来加速计算
    private static final int    REF_ELEMENT_SHIFT;
    // 反射拿到UNSAFE对象
    private static final Unsafe UNSAFE = Util.getUnsafe();

    static {
        // 返回Object数组中一个元素指针占用的内存大小
        final int scale = UNSAFE.arrayIndexScale(Object[].class);
        // 指针占4个字节
        if (4 == scale) {
            REF_ELEMENT_SHIFT = 2;
            // 指针占8个字节
        } else if (8 == scale) {
            REF_ELEMENT_SHIFT = 3;
        } else {
            throw new IllegalStateException("Unknown pointer size");
        }
        // BUFFER_PAD的取值为16/32,后续计算使用
        BUFFER_PAD = 128 / scale;
        // 计算数组首元素的偏移大小,往后偏移了128个字节,128/8=16
        REF_ARRAY_BASE = UNSAFE.arrayBaseOffset(Object[].class) + 128;
    }

    // 数组的最大下标,即bufferSize - 1
    private final   long      indexMask;
    // 对象数组,RingBuffer中实际数据载体
    private final   Object[]  entries;
    // RingBuffer的长度
    protected final int       bufferSize;
    // 序列器,分为单生产者和多生产者,用来在生产者和消费者之间传递数据,此处暂时略过
    protected final Sequencer sequencer;

    RingBufferFields(EventFactory<E> eventFactory, Sequencer sequencer) {
        this.sequencer = sequencer;
        this.bufferSize = sequencer.getBufferSize();

        if (bufferSize < 1) {
            throw new IllegalArgumentException("bufferSize must not be less than 1");
        }
        // bufferSize必须为2^N,否则进行报错
        if (Integer.bitCount(bufferSize) != 1) {
            throw new IllegalArgumentException("bufferSize must be a power of 2");
        }
        // 最大下标
        this.indexMask = bufferSize - 1;
        // 预留32/64个空槽位
        this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
        /**
         * 一次性填充满整个数组,从BUFFER_PAD的下一位开始;
         */
        fill(eventFactory);
    }

    private void fill(EventFactory<E> eventFactory) {
        // 预留BUFFER_PAD个空slot
        for (int i = 0; i < bufferSize; i++) {
            entries[BUFFER_PAD + i] = eventFactory.newInstance();
        }
    }

    @SuppressWarnings("unchecked")
    protected final E elementAt(long sequence) {
        // 元素内存偏移量=base + index * scale 
        // REF_ARRAY_BASE= UNSAFE.arrayBaseOffset(Object[].class) + 128;
        // 因为RingBuffer的长度为2^N,所以(sequence & indexMask) = sequence % bufferSize = index
        // index<<REF_ELEMENT_SHIFT = index << (2 ^ (log2(scale))) = index * scale
        return (E) UNSAFE.getObject(entries, REF_ARRAY_BASE + ((sequence & indexMask) << REF_ELEMENT_SHIFT));
    }
}

上述代码分析中,有几处关键点如下:

  • 所谓的环形缓冲区实质上就是一个连续数组,只是包装出来了环形的概念;
  • RingBuffer预分配内存时在数组的左右俩侧各预留了BUFFER_PAD个slot;
  • 内存预分配时从BUFFER_PAD处开始,在64位JDK中,假设开启指针压缩,则相当于左侧预留32个slot,右侧预留32个slot;
  • 在RingBuffer长度为2^N时,通过sequence & (bufferSize - 1)加速定位元素实际下标索引,通过结合<<操作实现乘法;
  • 首个元素的内存偏移位置是固定的,即128位,因为BUFFER_PAD * scale=128;

在MultiProducerSequencer中后续通过>>>实现除法,作用类似;

为何预留slot?

在现代CPU中,缓存行伪共享是一个经典问题,即CPU一次load的L1缓存行大小是固定的,如果两个不同的元素被load到同一缓存行中,那么任何一个元素的修改都会导致另一元素在其它CPU核对应的缓存失效,这个问题在很多博文中都有专门讲解,此处不再赘述。引用结论就是单个频繁修改元素尽量不和其它元素在同一缓存行中。
对于entries对象,左右两侧各预留了128 bytes,这就保证了缓存行在<=128bytes时,entries对象一定不会和其它对象共享缓存行。

Sequence是什么?

Sequence是针对RingBuffer中slot位置的一个描述,与AtomicLong的功能几乎一致,其在AtomicLong的基础上解决了缓存行伪共享的问题,其继承关系如下:



其核心元素为value,根据Java的内存分配,value两次各8个long对象,即大小为8 * 8=64 bytes,那么在缓存行<=64 bytes时,可以避免缓存行伪共享问题。
下面对其关键的两个方法做下简单分析:

/**
     * Perform an ordered write of this sequence.  The intent is a Store/Store barrier between this write and any previous store.
     *
     * putOrderedLong在执行时使用store/store内存屏障,因此其在可见性上会存在一定的延时,但是由于其未使用store/load屏障,因此其写入是无阻塞的
     */
    public void set(final long value) {
        UNSAFE.putOrderedLong(this, VALUE_OFFSET, value);
    }

    /**
     * Performs a volatile write of this sequence.  The intent is a Store/Store barrier between this write and any previous write and a
     * Store/Load barrier between this write and any subsequent volatile read.
     *
     * 本方法支持volatile语义,即在写入前插入store/store内存屏障,写入后插入store/load内存屏障,因此值的修改在其它线程是立即可见的
     */
    public void setVolatile(final long value) {
        UNSAFE.putLongVolatile(this, VALUE_OFFSET, value);
    }

Sequencer是什么?

前面讲完了Sequence,是时候推出Sequencer了,Sequencer是Disruptor中非常核心的一个概念,其作为生产者和消费者的桥梁,通过维护两者的sequence来实现数据的扭转;


sequencer.img

从继承体系上来看,sequencer主要分为单生产者和多生产者两类,这两者实现了各种free-lock的算法,此处以SingleProducerSequencer为例分析。

AbstractSequencer

该类主要属性说明如下:

// JDK提供的基于反射机制的volatile原子更新工具类,本更新器用来更新gatingSequences属性
private static final AtomicReferenceFieldUpdater<AbstractSequencer, Sequence[]> SEQUENCE_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(AbstractSequencer.class, Sequence[].class, "gatingSequences");

// RingBuffer中环形缓冲区的大小
protected final    int          bufferSize;
// 消费者等待策略
protected final    WaitStrategy waitStrategy;
// 生产者sequence,初始位置为-1
protected final    Sequence     cursor          = new Sequence(Sequencer.INITIAL_CURSOR_VALUE);
// 消费者sequence集合
protected volatile Sequence[]   gatingSequences = new Sequence[0];

该类主要方法说明如下:

    /**
     * 增加消费者sequence列表,该方法功能由SequenceGroups代理,本质上还是AtomicReferenceFieldUpdater进行更新
     */
    @Override
    public final void addGatingSequences(Sequence... gatingSequences) {
        SequenceGroups.addSequences(this, SEQUENCE_UPDATER, this, gatingSequences);
    }

    /**
     * 移除消费者sequence列表,该方法功能由SequenceGroups代理,本质上还是AtomicReferenceFieldUpdater进行更新
     */
    @Override
    public boolean removeGatingSequence(Sequence sequence) {
        return SequenceGroups.removeSequence(this, SEQUENCE_UPDATER, sequence);
    }

    /**
     * 获取消费者sequence列表中最小的索引下标值,cursor.get()为生产者下标值
     */
    @Override
    public long getMinimumSequence() {
        return Util.getMinimumSequence(gatingSequences, cursor.get());
    }

SingleProducerSequencer

SingleProducerSequencer中最主要的属性为nextValue和cachedValue,其中nextValue表示获取下一个可用slot的索引下标,而cachedValue则是当前消费者消费最慢slot的索引下标,下面以next(int n)方法为例分析生产者获取可用sequence的逻辑。

/**
 * 申请N个可用slot,用来存放生产出来的数据
 * @param n 可用sequence的个数
 */
public long next(int n) {
        // 申请个数不能<1,不能>bufferSize
        if (n < 1 || n > bufferSize) {
            throw new IllegalArgumentException("n must be > 0 and < bufferSize");
        }

        // 当前slot下标,初始时为-1
        long nextValue = this.nextValue;

        // 申请可用槽位的最大下标,默认一次申请1个,所以初始时位0
        long nextSequence = nextValue + n;
        // 倒转1轮,与消费者进行比较
        long wrapPoint = nextSequence - bufferSize;

        // 消费者中记录的最小slot下标,默认为-1
        long cachedGatingSequence = this.cachedValue;

        // wrapPoint > cachedGatingSequence,主要用来判断是否出现环路,即生产者生产速度过快,追上了消费者;
        // cachedGatingSequence > nextValue,主要用来判断是否消费者消费过快,追上了生产者;
        if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue) {
            // 设置生产者要使用的slot下标,底层为volatile语义
            cursor.setVolatile(nextValue);  // StoreLoad fence
            
            long minSequence;
            // 重新最小slot值,此处分两种情况讨论:
            // 如果nextValue<gatingSequences最小值,说明消费者消费过快,那么此时wrapPoint=nextValue + n-bufferSize<nextValue,直接跳出循环,消费者处直接等待;
            // 如果nextValue>gatingSequences最小值,那么判断是否出现环路,如果不出现,那么直接分配,否则,生产者等待;
            while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue))) {
                // 生产者等待
                LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
            }

            this.cachedValue = minSequence;
        }
        // 更新生产者已使用的slot下标
        this.nextValue = nextSequence;

        return nextSequence;
}

从上述的代码分析来看,申请可用slot主要通过生产者和消费者的sequence进行比较,单生产者与多生产者的差别仅在于多生产者对sequence的更新可能产生争用,因此使用CAS机制进行更新;

publishEvent

事件发布也是RingBuffer提供的一个重要功能,其相当于为生产者提供了数据操作的入口,通过前面的继承体系可以看出,事件相关操作是由EventSink接口提供的,下面对其先进行一个简单的分析。

public void publishEvent(EventTranslator<E> translator) {
        // 占用slot位置
        final long sequence = sequencer.next();
        // 发布事件
        translateAndPublish(translator, sequence);
}
......
private void translateAndPublish(EventTranslator<E> translator, long sequence) {
        try {
            // 事件转换
            translator.translateTo(get(sequence), sequence);
        } finally {
            // 更新生产者sequence的值
            sequencer.publish(sequence);
        }
}
......
public void publish(long sequence) {
        // 更新生产者的value
        cursor.set(sequence);
        // 唤醒消费者线程,这个与等待策略有关,此处先不具体讨论
        waitStrategy.signalAllWhenBlocking();
}

结合上面next()的实现源码,我们大致可以得出如下结论,以单事件发布为例,其是一个两阶段操作

  • 预占用slot,更新sequencer的nextValue,这样下次分配时从新的nextValue往下分配
  • 对slot的具体数据进行更新,同时更新生产者的sequence

由于是单生产者,这种操作方式不会产生并发问题。但是在多生产者中,逻辑会有问题,比如如果在第二步中更新sequence,在一定程度上会存在顺序错乱的问题,因此在预申请的时候就要更新生产者sequence,同时如何判断数据发布成功了呢?我们后面揭晓。

MultiProducerSequencer

    // UNSAFE对象实例
    private static final Unsafe UNSAFE = Util.getUnsafe();
    // 获取int数组的首个元素偏移位置
    private static final long   BASE   = UNSAFE.arrayBaseOffset(int[].class);
    // 返回一个int数组中对象指针占用的内存大小
    private static final long   SCALE  = UNSAFE.arrayIndexScale(int[].class);

    // 缓存的消费者最慢slot的下标
    private final Sequence gatingSequenceCache = new Sequence(Sequencer.INITIAL_CURSOR_VALUE);

    // availableBuffer tracks the state of each ringbuffer slot
    // see below for more details on the approach
    private final int[] availableBuffer;

    // ringbuffer最大下标
    private final int indexMask;

    // log2(bufferSize)
    private final int indexShift;

通过这些属性定义,我们似乎有一点熟悉,比如BASE、SCALE、indexMask、indexShift这些概念,实际上这些值主要也是为了加速计算,其中比较让人懵圈的是int数组availableBuffer,所以可以想象BASE这些主要是为了对其进行内存操作;

public MultiProducerSequencer(int bufferSize, final WaitStrategy waitStrategy) {
        super(bufferSize, waitStrategy);
        // 初始化availableBuffer大小为RingBuffer的大小
        availableBuffer = new int[bufferSize];
        indexMask = bufferSize - 1;
        indexShift = Util.log2(bufferSize);
        initialiseAvailableBuffer();
}

private void initialiseAvailableBuffer() {
         // 这个写法有点奇怪,提前分配内存,值为-1
        for (int i = availableBuffer.length - 1; i != 0; i--) {
            setAvailableBufferValue(i, -1);
        }

        setAvailableBufferValue(0, -1);
}

OK,分析到这里,我们大致了解了多生产者的一些关键信息,下面回到在单生产者发布事件的问题上来继续探究,我们知道无论是哪种生产者模式,第一步都是申请可用的slot,那下面直接上多生产者申请slot的代码。

public long next(int n) {
        // 基础的逻辑判断
        if (n < 1 || n > bufferSize) {
            throw new IllegalArgumentException("n must be > 0 and < bufferSize");
        }
        // 生产者实际的sequence值 
        long current;
        // 申请可用slot的最大值
        long next;

        do {
            // 实际sequence,初始时为-1
            current = cursor.get();
            // 申请可用slot的最大值,初始时为0
            next = current + n;
            
            // 下面与单生产者类似
            long wrapPoint = next - bufferSize;
            long cachedGatingSequence = gatingSequenceCache.get();
            // 这里补充一点,cachedGatingSequence是缓存的上一次可能出现环路或消费过快时的消费者最慢slot的下标,正常申请时可能存在多轮未更新
            // 因此理论上cachedGatingSequence可能并不是最新的值,且其值相比实际值较小;
            if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) {
                // 获取最小的sequence值
                long gatingSequence = Util.getMinimumSequence(gatingSequences, current);
                // 如果出现环路,等待然后继续循环
                if (wrapPoint > gatingSequence) {
                    LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
                    continue;
                }
                // 未出现环路,更新消费者的sequence值
                gatingSequenceCache.set(gatingSequence);
            // CAS更新生产者的sequence,实现free-lock
            } else if (cursor.compareAndSet(current, next)) {
                // 更新成功跳出
                break;
            }
        } while (true);
      
        return next;
}

从上述可以看出,next方法在多生产者中永远都依赖生产者的实际sequence值进行计算,且占用成功后实时更新;

这里是与单生产者模式的第一个差别,单生产者中计算依赖的是其自身的nextValue值,其通过publish才会实际更新生产者游标,所以单生产者是一个典型的二阶段提交;

对于多生产者来说,由于生产者游标被实时更新掉了,那消费者依据什么来判断数据已经替换(发布)成功了呢?查看MultiProducerSequencer的publish方法,代码如下:

public void publish(final long sequence) {
        // 关键代码
        setAvailable(sequence);
        waitStrategy.signalAllWhenBlocking();
}

继续往下翻看setAvailable的代码:

private void setAvailable(final long sequence) {
        setAvailableBufferValue(calculateIndex(sequence), calculateAvailabilityFlag(sequence));
}
...
private int calculateAvailabilityFlag(final long sequence) {
        // 计算值=sequence / bufferSize,即该sequence属于哪一圈
        return (int) (sequence >>> indexShift);
}

private int calculateIndex(final long sequence) {
        // 计算值=sequence % bufferSize,即该sequence的实际数组下标
        return ((int) sequence) & indexMask;
}
// 由上可知,availableBuffer中记录了对应sequence的实际圈数(round)
private void setAvailableBufferValue(int index, int flag) {
        // 计算偏移量
        long bufferAddress = (index * SCALE) + BASE;
        UNSAFE.putOrderedInt(availableBuffer, bufferAddress, flag);
}

由上我们可知,availableBuffer中记录了最新sequence对应的圈数,这里我们先临时插入一个消费者的代码片段,在Disruptor中,消费者的接口主要为EventProcessor,这里以WorkProcessor为例,代码如下:

if (cachedAvailableSequence >= nextSequence) {
  // 获取下一个工作序列
  event = ringBuffer.get(nextSequence);
  // 处理序列
  workHandler.onEvent(event);
  // 更新标记为true
  processedSequence = true;
} else {
  // 获取可处理的最大sequence序列
  cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence);
}
...
public long waitFor(final long sequence) throws AlertException, InterruptedException, TimeoutException {
  checkAlert();
  // 获取可用的序列
  long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);

  if (availableSequence < sequence) {
    return availableSequence;
  }
  // 关键在这里 !import
 return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}

可知消费者消费数据时强依赖sequencer.getHighestPublishedSequence的方法,多生产者的代码实现如下:

public long getHighestPublishedSequence(long lowerBound, long availableSequence) {
        for (long sequence = lowerBound; sequence <= availableSequence; sequence++) {
            // 关键代码 !import
            if (!isAvailable(sequence)) {
                return sequence - 1;
            }
        }
        return availableSequence;
}
...
public boolean isAvailable(long sequence) {
        // %,计算实际的下标
        int index = calculateIndex(sequence);
        // 计算其round
        int flag = calculateAvailabilityFlag(sequence);
        long bufferAddress = (index * SCALE) + BASE;
        // 判断其是否与标识位相同,如果不相同,则代码数据未publish完成
        return UNSAFE.getIntVolatile(availableBuffer, bufferAddress) == flag;
}

由上整个分析基本结束,即消费者依靠availableBuffer的值来判断该数据是否已经发布完毕;

总结

本文主要对RingBuffer的数据结构、生产者以及数据发布流程进行了一个大致的分析,但其中有很多的细节还未来得及分析,比如生产者和消费者之间是如何进行数据联动的,Barrier在其中扮演的角色又是什么,这些留到下一篇文章进行深入探讨。

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

推荐阅读更多精彩内容