4、内存队列设计:无锁队列(Disruptor模式)、有界队列与背压机制、内存屏障与伪共享问题

做市商系统的数据管道,说白了就是一条高速公路。行情数据、订单数据、成交回报,全在这条路上跑。如果路堵了,系统就崩了。我见过不少团队,一开始用JDK自带的BlockingQueue,觉得够用。结果行情一爆发,延迟飙升,CPU打满,最后只能重启。

嗯,今天我们就来聊聊,怎么设计一条真正能扛住高频行情的「内存高速公路」。

4.1 无锁队列:Disruptor模式

先问一个问题:为什么BlockingQueue在高并发下会慢?

答案很简单——锁。锁会导致线程阻塞、上下文切换、缓存失效。你想想看,行情每微秒来一笔,你却在排队等锁,这怎么行?

Disruptor模式的核心思路,就是「无锁」。它用环形缓冲区(Ring Buffer)代替传统队列,生产者直接往槽位写数据,消费者直接从槽位读数据。没有锁,全靠内存屏障和CAS操作来保证可见性。

核心要点:Disruptor不是队列,而是一种「事件处理器」架构。它把数据当作事件,在Ring Buffer里流转。

我在项目中遇到过一个问题:用Disruptor处理行情数据,一开始延迟降到了微秒级,但跑了一段时间后,偶尔会出现「事件丢失」。排查了半天,发现是消费者处理速度跟不上生产者,导致槽位被覆盖。后来加了「等待策略」才解决。

Disruptor的几种等待策略,我列一下:

策略名称 适用场景 CPU消耗
BlockingWaitStrategy 低并发、低延迟要求
SleepingWaitStrategy 中等并发、平衡型
YieldingWaitStrategy 高并发、低延迟
BusySpinWaitStrategy 极致低延迟、CPU密集 极高

我个人习惯,在行情处理中用YieldingWaitStrategy。它让消费者线程在没数据时主动让出CPU,但不会进入睡眠。延迟和CPU消耗之间,算是个不错的平衡点。

4.2 有界队列与背压机制

无锁队列解决了并发问题,但还有一个更棘手的问题——生产者太快,消费者太慢。

做市商系统里,行情爆发时,每秒可能涌入几十万笔数据。如果消费者处理不过来,队列会无限膨胀,最终OOM。这就是为什么我坚持用「有界队列」。

有界队列,说白了就是给队列设一个最大容量。满了怎么办?这就是背压机制要解决的问题。

常见的背压策略有三种:

  • 丢弃策略:队列满了,直接丢弃新数据。适合行情数据,丢几笔无所谓。
  • 阻塞策略:生产者等待,直到队列有空位。适合订单处理,不能丢数据。
  • 降级策略:触发背压时,降低生产速率或切换备用通道。

我的经验:在行情管道中,我一般用「丢弃策略」+「监控告警」。队列满了就丢,但必须记录丢了多少笔。如果丢包率超过阈值,立刻告警。这样既保证了系统不崩,又能及时发现异常。

我曾经踩过一个坑:用无界队列接收行情,结果某次行情异常爆发,队列里积压了上千万笔数据,GC直接STW了十几秒。从那以后,我再也不敢用无界队列了。

4.3 内存屏障与伪共享问题

这两个问题,是并发编程里的「隐形杀手」。很多做市商系统的性能问题,追根溯源,都跟它们有关。

内存屏障,也叫内存栅栏。它是一条CPU指令,用来保证内存操作的顺序性。简单说,就是告诉CPU:你先把这个写完了,再让别的线程看到。

在Java里,volatile关键字和Unsafe.putOrderedXXX都会插入内存屏障。Disruptor之所以快,就是因为它精确控制了内存屏障的插入位置,而不是像BlockingQueue那样用锁来保证可见性。

伪共享,这个问题更隐蔽。CPU缓存是以「缓存行」为单位加载的,一般是64字节。如果两个变量恰好在同一个缓存行里,而且被不同线程修改,就会导致缓存行频繁失效,性能暴跌。

我举个例子:

// 伪共享的典型例子
class Counter {
    public volatile long value1;
    public volatile long value2;
}

如果线程A修改value1,线程B修改value2,它们虽然没共享变量,但共享了缓存行。每次修改都要同步,性能直接掉一个数量级。

解决方案很简单——填充字节,让变量独占缓存行。

// 解决伪共享:填充到64字节
class PaddedCounter {
    public volatile long value1;
    public long p1, p2, p3, p4, p5, p6, p7; // 填充
    public volatile long value2;
}

注意:Java 8之后,可以用@Contended注解自动填充。但要注意,这个注解是JVM内部用的,生产环境需要加JVM参数-XX:-RestrictContended才能生效。

我在做订单簿模块时,就遇到过伪共享问题。两个线程分别更新买一价和卖一价,性能始终上不去。后来用@Contended一标,延迟直接降了40%。

4.4 知识体系总览

下面这张图,是我对内存队列设计的理解。你可以把它当作一个「决策树」:

内存队列设计知识体系 内存队列设计 无锁队列 (Disruptor) 有界队列与背压 内存屏障与伪共享 Ring Buffer + CAS + 内存屏障 等待策略:Blocking / Yielding / BusySpin 固定容量 + 满时策略 丢弃 / 阻塞 / 降级 volatile / Unsafe / 内存栅栏指令 伪共享:缓存行填充 / @Contended 核心目标:低延迟 + 高吞吐 + 不丢数据 三者缺一不可,但需要根据场景做取舍

这张图里,三个分支是并列关系。实际项目中,你往往需要同时考虑它们。比如用Disruptor时,也要考虑背压;用有界队列时,也要注意伪共享。

一句话总结:内存队列设计,不是选一个框架就完事了。你得理解底层原理,才能做出真正高性能的系统。

好了,这一章就到这里。下一章我们会聊「数据序列化与反序列化」,看看怎么在内存和网络之间,高效地搬运数据。


公众号:蓝海资料掘金营,微信deep3321