# Disruptor
# 简介
# 它能够在一个线程里每秒处理6百万订单。业务逻辑处理器完全是运行在内存中,使用事件源驱动方式。
# 本次内容基于 version
# com.lmax:disruptor:3.4.2
# 概念
# 无锁设计
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。
# 元素位置定位
数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
# 环形数组结构
为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。
# 核心类
# RingBuffer
# 结构
# 环形数组(数组内存连续),存储事件(数据)Object[]
# 维护 自增 sequence 来获取数据的索引 查找数据;(seq%bufferSize);
实际是位运算达到取余的效果 类似hashMap 的索引计算
# 对象数组实际大小 的前后都加了个BUFFER_PAD (4/8 一个对象应用的大小)的空间
# 读取
# customer 通过 下一个要读取的位置 调用SequenceBarrier # waitFor 获取最大可读取的序号;
(这里会执行WaitStrategy 等待策略. )
通过获取的序列号轮休读取(消费)数据并更新消费的进度(Cursored);
这里可以并发消费 不会有锁和竞争;
# 写入
# 单个生产者
# 1. 两阶段提交 (two-phase commit)
# 1.1. 生产者需要申请 buffer 里的下一个节点(更新 next)
# 1.1.1. Sequencer.next
# 1.1.1.1. 判断要写入的节点是否有消费者在消费,如果有就需要自旋等待
# 1.2. 当生产者向节点写完数据,提交(更新 Cursore)。
# 多个生产者
# 1. 多生产者 会在提交的时候验证 cursore 必须是提交索引的前一个,才可以成功 否则自旋转
# 2. 生产者在不同的时间完成数据写入,但是 Ring Buffer 的内容顺序总是会遵循 nextEntry() 的初始调用顺序。也就是说,如果一个生产者在写入 Ring Buffer 的时候暂停了,只有当它解除暂停后,其他等待中的提交才会立即执行。
### Sequence# 通过顺序递增的序号来编号管理通过其进行交换的数据
# 防止不同的 Sequence 之间的CPU缓存伪共享(Flase Sharing)问题。
Related to: 伪共享
# Sequencer
# 继承树
# 
# 职责
# 定义在生产者和消费者之间快速、正确地传递数据的并发算法。
# 维护 ringbuffer 的索引获取 , 生产者需要先获取索引,才可以生产数据填入
# 维护cursor(游标sequence,即 最快生产者生产的的位置索引), 提供读取方法
Related to: Sequence
# 维护消费者消费索引数组,当生产者遇到消费者占用的索引时 执行等待策略 自旋,等消费者完成时 在返回给生产者
# SingleProducerSequencer
# MultiProducerSequencer
# int[] availableBuffer
# 1. 维护 ringbuffer的状态 是否可读,是否完成了写入
# SequenceBarrier
# 获取发布后的的 ringBuffer sequence 序号
# 状态变更提醒
# 消费者等待策略 执行
# WaitStrategy
# 消费者等待策略
# EventProcessor
# 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop) 调用 EventHandler。
# EventHandler
# 定义的事件处理接口,由用户实现,用于处理事件,是 Consumer 的真正实现
# ConsumerRepository
# 维护消费者调用链as :c 需要等待 a,c 完成后执行 (a,c 的执行结果存储在 ringbuffer 中)
# 补充
# 缓存行
缓存是由缓存行组成的,缓存行是2的整数幂个连续字节,一般为32-256个字节, 通常是64字节,并且它有效地引用主内存中的一块地址。 一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。 对缓存的操作通常是以一整个缓存为单位进行的, 所有一个写频繁的变量独占一个缓存行可以提高效率; 可以通过填充的方式占满一个缓存行;#### 伪共享
# 如果两个独立的线程同时写两个不同的值(内存地址相邻,在同一个缓存行上)会更糟。
因为每次线程对缓存行进行写操作时,
每个内核都要把另一个内核上的缓存块无效掉并重新读取里面的数据。
你基本上是遇到两个线程之间的写冲突了,尽管它们写入的是不同的变量。
这就造成和缓存一直是失效的状态 就是伪共享的情况.
# 缓存行填充
# 你会看到Disruptor消除这个问题,至少对于缓存行大小是64字节或更少的处理器架构来说是这样的
(注:有可能处理器的缓存行是128字节,那么使用64字节填充还是会存在伪共享问题),
通过增加补全(6个 long (6*8)+ value(1*8)+对象头(2 *8字节) = 64)来确保ring buffer的序列号不会和其他东西同时存在于一个缓存行;
因此没有伪共享,就没有和其它任何变量的意外冲突,没有不必要的缓存未命中。
# Java内存布局
# 对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位
(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。
如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。
因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序: doubles (8) 和 longs (8) ints (4) 和 floats (4) shorts (2) 和 chars (2) booleans (1) 和 bytes (1) references (4/8) <子类字段重复上述顺序>
# java实例数据(各个成员变量)
# 1. boolean、byte = 1字节
short 、char = 2字节
int、float = 4字节
long、double = 8字节
引用类型 = 4/8字节(不同位数的机器有所不同)
# 内存屏障
它是一个CPU指令。基本上,它是这样一条指令: a)确保一些特定操作执行的顺序; b)影响一些数据的可见性(可能是某些指令执行后的结果)。 编译器和CPU可以在保证输出结果一样的情况下对指令重排序, 使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行, 后于这个命令的必须后执行。 内存屏障另一个作用是强制更新一次不同CPU的缓存。 例如,一个写屏障会把这个屏障前写入的数据刷新到缓存, 这样任何试图读取该数据的线程将得到最新值, 而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。