高性能内存队列-Disruptor
Disruptor是基于环形数组的内存消息队列,相比JDK提供的阻塞队列
- 效率更高
- 应用场景更多,除基本的发布订阅/点对点模式外,还可实现消费者间依赖,实现更复杂业务
一、为什么高效
-
无锁化,普通阻塞队列ArrayBlockingQueue(为了防止生产者速度过快导致内存溢出,只能选择有界队列,同时为提高读取效率,JDK内只有它适合做队列),使用的是重量级锁Lock,生产者消费者put/take间是互斥关系,头尾指针竞争激烈,竞争将导致线程经常挂起,效率很低。Disruptor生产者消费者之间不互斥,各自维护各自的序列号,并且生产/消费者之间使用CAS乐观锁,不存在线程切换的开销,在持有锁时间较短的场景下效率很高(单生产者/消费者模型连CAS都不用)
-
存储空间方案优化,普通阻塞队列take后元素主动置null,引发GC。Disruptor为循环覆盖,不存在GC
-
利用先行填充消除伪共享
CPU和主内存之间是多级缓存,缓存越靠近主内存越慢,缓存最小存储单元是缓存行,一般是64字节,CPU读取变量时会加载它相邻的变量到同一个缓存行内,这也是为什么数组读取效率高的原因之一,也是为啥Disruptor要使用数组结构
但是这会存在一个问题,缓存行内任何一个变量改变,加载同一缓存行数据的其他线程内缓存都会失效,要重新加载数据到缓存
Disruptor的解决办法就是预先填充,如RingBuffer填充7个long类型数据,为保证cursor指针在缓存行内只加载一个有用数据,其他7个没人修改所以不会造成缓存失效
二、环形数组是怎么构造出来的
首先明确,所谓环形数组并不是真的环形结构,单纯是个普通数组,只不过通过get方法来控制获取的数据位,看起来就像是环形一样
如何确定元素位置
protected final E elementAt(long sequence){
return (E) UNSAFE.getObject(entries, REF_ARRAY_BASE + ((sequence & indexMask) << REF_ELEMENT_SHIFT));
}
REF_ARRAY_BASE是基地址 UNSAFE.arrayBaseOffset(Object[].class)
(sequence & indexMask)可以确定数组下标
REF_ELEMENT_SHIFT为数组每个元素的存储空间大小 UNSAFE.arrayIndexScale(Object[].class)
((sequence & indexMask) << REF_ELEMENT_SHIFT)左移目的是往前跳N个格,每个格大小是REF_ELEMENT_SHIFT,结合起来构成了偏移量
基地址 + 偏移量,就能找到元素
三、序号是一直递增的,会不会爆?
不会,序号是long型,100万QPS的处理速度,也需要30万年才能用完 9223372036854775807 / 31536000 * 1000000 = 292471年
类图
基本使用
配置/启动 Disruptor
@PostConstruct
private void start() {
Disruptor disruptor = new Disruptor<>(new PushMsgEventFactory(), bufferSize, Executors.newCachedThreadPool());
WorkHandler[] workers = {new PushMsgHander()};
// 多消费者模式
disruptor.handleEventsWithWorkerPool(workers);
// 开启队列
disruptor.start();
}
// 事件工厂,环形数组初始化时会用该工厂填充整个桶(即事先填好空对象)
public class PushMsgEventFactory implements EventFactory<PushMsgEvent> {
@Override
public PushMsgEvent newInstance() {
return new PushMsgEvent();
}
}
// 生产者
private void pushMsg(PushMsgData pushMsgData) {
// 获取环形队列RingBuffer(disruptor实例自己想办法注入)
RingBuffer<PushMsgEvent> ringBuffer = disruptor.getRingBuffer();
// 获取下一个序列号
long sequence = ringBuffer.next()