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时,也要考虑背压;用有界队列时,也要注意伪共享。
一句话总结:内存队列设计,不是选一个框架就完事了。你得理解底层原理,才能做出真正高性能的系统。
好了,这一章就到这里。下一章我们会聊「数据序列化与反序列化」,看看怎么在内存和网络之间,高效地搬运数据。
公众号:蓝海资料掘金营,微信deep3321