4、锁优化与无锁编程:细粒度锁、读写锁、CAS原子操作、无锁队列(Disruptor模式)在撮合引擎中的应用
说到撮合引擎的锁优化,我得先坦白一件事。
我早年做第一版撮合引擎时,图省事,直接上了个大粗锁。整个订单簿一把锁,撮合、撤单、查询全串行。结果呢?压测到每秒5000笔订单,CPU直接飙到90%,延迟从5微秒跳到200微秒。客户骂娘,我背锅。
从那以后,我算是彻底明白了——锁,是性能的第一杀手。但不用锁又不行,数据一致性谁来保证?
所以今天咱们聊聊,怎么在保证正确性的前提下,把锁的代价降到最低。
核心观点:锁不是不能用,而是要用对地方、用对粒度、用对时机。无锁编程不是银弹,但特定场景下确实能带来数量级的提升。
4.1 细粒度锁:别一把锁锁死全世界
什么叫细粒度锁?说白了,就是把一把大锁拆成多把小锁。
举个例子。订单簿里通常有多个价格档位,每个档位有自己的买卖队列。如果你用一把锁锁住整个订单簿,那一个档位的操作会阻塞其他所有档位。这合理吗?不合理。
我建议的做法是:每个价格档位一把锁。
// 粗粒度锁 - 不推荐
public class OrderBook {
private final ReentrantLock globalLock = new ReentrantLock();
private Map<Double, OrderQueue> priceLevels;
public void addOrder(Order order) {
globalLock.lock();
try {
// 操作某个价格档位
priceLevels.get(order.getPrice()).add(order);
} finally {
globalLock.unlock();
}
}
}
// 细粒度锁 - 推荐
public class OrderBook {
private final Map<Double, PriceLevel> priceLevels;
public void addOrder(Order order) {
PriceLevel level = priceLevels.get(order.getPrice());
level.lock(); // 只锁当前档位
try {
level.add(order);
} finally {
level.unlock();
}
}
}
我在项目中遇到过一个问题:某个热门价格档位竞争激烈,其他档位却空闲。这时候细粒度锁的优势就出来了——热门档位忙它的,冷门档位不受影响。整体吞吐量提升了3倍多。
避坑指南:细粒度锁虽然好,但别分得太细。我曾经把每个订单都单独加锁,结果锁对象太多,内存开销暴涨,GC频繁触发,反而更慢了。适度就好。
4.2 读写锁:读多写少的场景是绝配
撮合引擎里有个典型场景:行情查询。
你想想看,一个订单簿上,可能每秒有1000次查询,但只有100次撮合写入。读操作远多于写操作。这时候用普通的互斥锁,读和读之间也要互相等待,太浪费了。
读写锁就是干这个的——读读不互斥,读写互斥,写写互斥。
public class OrderBookSnapshot {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private Snapshot currentSnapshot;
// 读操作 - 可以并发
public Snapshot getSnapshot() {
rwLock.readLock().lock();
try {
return currentSnapshot.copy();
} finally {
rwLock.readLock().unlock();
}
}
// 写操作 - 必须独占
public void updateSnapshot(OrderBook book) {
rwLock.writeLock().lock();
try {
currentSnapshot = book.buildSnapshot();
} finally {
rwLock.writeLock().unlock();
}
}
}
我记得有一次,一个团队用普通锁做行情推送,延迟波动很大。换成读写锁后,读操作几乎零等待,写操作虽然还是独占,但写频率低,影响微乎其微。整体延迟从50微秒降到了5微秒。
注意:读写锁不是万能的。如果写操作占比超过20%,读写锁的优势就不明显了。因为写锁会阻塞所有读锁,写多了反而比普通锁更差。我见过有人不分青红皂白全上读写锁,结果性能反而下降。
4.3 CAS原子操作:无锁的基石
CAS,全称Compare-And-Swap,翻译过来就是「比较并交换」。它是个CPU指令级别的原子操作。
为什么说它是无锁的基石?因为很多无锁数据结构,底层都是靠CAS实现的。
// 使用CAS实现线程安全的计数器
public class AtomicCounter {
private volatile int value;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(
AtomicCounter.class.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
public int incrementAndGet() {
int current;
do {
current = unsafe.getIntVolatile(this, valueOffset);
// CAS: 如果当前值等于期望值,就更新为新值
} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
return current + 1;
}
}
CAS有个经典问题叫ABA问题。什么意思呢?就是线程1读到值是A,准备CAS时,线程2把A改成B又改回A。线程1的CAS成功了,但它不知道中间发生过变化。
嗯,这里要注意。在撮合引擎里,ABA问题可能导致订单状态错乱。解决办法是加版本号,或者用AtomicStampedReference。
我的经验:CAS适合轻量级的原子操作,比如计数器、状态标记。但别用它实现复杂的业务逻辑,否则代码会变得极其难读且容易出错。我曾经用CAS实现了一个无锁的订单状态机,调试了整整三天才跑通,后来还是换回了锁——因为维护成本太高了。
4.4 无锁队列:Disruptor模式
说到无锁编程,就绕不开Disruptor。这是LMAX交易所开源的一个高性能无锁队列框架,号称单线程每秒能处理600万笔订单。
Disruptor的核心思想其实很简单:用环形缓冲区 + 预分配内存 + 序列号机制,避免锁和GC。
Disruptor的几个关键点:
- 环形缓冲区:固定大小的数组,避免动态扩容和内存分配
- 预分配事件对象:创建时就分配好内存,运行时复用,避免GC
- 序列号:用CAS更新序列号,代替锁
- 批处理:消费者可以一次拉取多个事件,减少上下文切换
// Disruptor 在撮合引擎中的简化使用
public class MatchingEngine {
private final RingBuffer<OrderEvent> ringBuffer;
private final EventHandler<OrderEvent> handler;
public MatchingEngine() {
// 创建环形缓冲区,大小必须是2的幂
this.ringBuffer = RingBuffer.createSingleProducer(
OrderEvent::new, // 事件工厂
1024, // 缓冲区大小
new YieldingWaitStrategy() // 等待策略
);
this.handler = (event, sequence, endOfBatch) -> {
// 处理订单撮合逻辑
matchOrder(event.getOrder());
};
// 绑定消费者
ringBuffer.addGatingSequences(handler);
}
// 发布订单 - 无锁操作
public void submitOrder(Order order) {
long sequence = ringBuffer.next(); // CAS获取序列号
try {
OrderEvent event = ringBuffer.get(sequence);
event.setOrder(order);
} finally {
ringBuffer.publish(sequence); // CAS发布
}
}
}
我在项目中用Disruptor替换了原来的BlockingQueue,延迟从30微秒降到了3微秒。为什么?因为BlockingQueue底层用了ReentrantLock,而Disruptor全程无锁。
注意:Disruptor虽然快,但使用门槛高。你得自己管理事件对象的生命周期,处理序列号溢出问题,还要选择合适的等待策略。我见过有人把Disruptor用成了BlockingQueue,结果性能还不如原来的——因为等待策略选错了。
4.5 实战建议:怎么选?
说了这么多,到底该怎么选?我总结了一张表:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 订单簿不同档位操作 | 细粒度锁 | 减少锁竞争,提高并发度 |
| 行情快照查询(读多写少) | 读写锁 | 读读不互斥,提升查询性能 |
| 计数器、状态标记 | CAS原子操作 | 轻量级,无阻塞 |
| 订单事件传递(高吞吐) | Disruptor无锁队列 | 无锁、无GC、批处理 |
| 复杂业务逻辑(状态机) | 还是用锁吧 | 无锁实现太复杂,容易出bug |
最后说一句:不要为了无锁而无锁。我见过一个团队,花了两周时间把一段代码改成无锁的,结果性能只提升了5%,代码却变得谁都看不懂。值吗?不值。
锁优化也好,无锁编程也罢,最终目标都是在保证正确性的前提下,把延迟降到最低。选对方案,比炫技重要得多。
我的习惯:先上锁,跑压测,看瓶颈在哪。如果锁竞争不严重,就别动。如果锁是瓶颈,再针对性地优化。别一开始就搞无锁,那是最后的手段。
公众号:蓝海资料掘金营,微信deep3321