4. 价格优先与时间优先:撮合队列的核心法则

大家好,我是你们的老朋友。今天我们来聊聊撮合引擎里最基础、也最核心的两个原则——价格优先和时间优先。

说实话,我见过不少刚入行的同学,觉得这两个原则太简单了,不就是“价高者得、先来后到”嘛。但实际在项目中,细节远比想象中复杂。我曾经在一个高频交易系统里,就因为时间戳精度不够,导致订单排序出了大问题,差点让客户亏了几百万。嗯,从那以后,我对这两个原则的理解就深刻多了。

核心结论:价格优先是“第一筛选条件”,时间优先是“同价下的决胜规则”。两者结合,构成了公平、高效的撮合排序策略。

4.1 价格优先原则详解

价格优先,说白了就是——谁的报价更优,谁就先成交。

对于买单,价格越高越优先;对于卖单,价格越低越优先。这个逻辑很直观:买方想便宜买,卖方想贵卖,但市场撮合要促成交易,所以必须让“最想成交”的人先成交。

具体规则:

  • 买单队列:按价格从高到低排序。最高价的买单排在最前面。
  • 卖单队列:按价格从低到高排序。最低价的卖单排在最前面。

举个例子,假设当前市场上有以下买单:

订单ID 价格(元) 数量 时间
A 10.05 100 09:30:01.000
B 10.02 200 09:30:00.500
C 10.05 150 09:30:00.800

按照价格优先原则,买单A和C都是10.05元,高于B的10.02元。所以A和C会排在B前面。至于A和C谁先谁后?那就是时间优先的事了。

我的经验:在实际系统中,价格优先的排序通常用红黑树或跳表实现。我习惯用红黑树,因为插入和删除都是O(log n),性能稳定。但要注意,价格档位可能非常多,极端行情下会有几千个不同价格,这时候内存管理要格外小心。

4.2 时间优先原则详解

时间优先,就是“先来后到”。当价格相同时,谁先提交订单,谁就排在前面。

你想想看,如果没有时间优先,那同价位的订单怎么排?按订单ID?按用户等级?那都不公平。时间优先是最简单、最公平的规则。

关键细节:

  • 时间戳必须精确到毫秒甚至微秒级别。我见过用秒级时间戳的系统,结果同一秒内几十个订单无法区分先后,只能随机排序——这绝对是个坑。
  • 时间戳的生成时机也很重要。是客户端提交时间?还是服务器接收时间?我个人建议用服务器接收时间,因为客户端时间不可信。

避坑指南:我曾经在一个项目中,用了客户端时间戳。结果有个用户自己改了系统时间,把订单时间改到前一天,导致他的订单一直排在前面。后来我们改成服务器时间戳,并加了序列号,才彻底解决这个问题。

回到上面的例子,买单A和C价格相同(10.05元),但C的时间是09:30:00.800,A是09:30:01.000。所以C比A更早,C排在A前面。最终排序是:C → A → B。

4.3 两者结合的综合排序策略

现在我们把两个原则合起来。综合排序策略其实就一句话:先按价格分档,再按时间排序。

具体流程是这样的:

  1. 所有买单按价格从高到低分成多个价格档位。
  2. 每个价格档位内部,按时间从早到晚排序。
  3. 撮合时,从最高价档位的最早订单开始匹配。

为了让你更直观地理解,我画了一张流程图:

综合排序策略流程图 订单进入队列 价格优先:按价格分档 时间优先:同价内按时间排序 生成有序撮合队列 注:价格优先是首要条件,时间优先是次要条件。

你看,整个流程很清晰。但实际实现时,有几个细节要注意:

实现要点:

  • 数据结构选择:我建议用“价格桶 + 时间有序链表”的组合。每个价格对应一个桶,桶内用链表按时间排序。这样插入和查询都很快。
  • 并发处理:撮合引擎通常是多线程的。对队列的读写要加锁,或者用无锁队列。我个人偏向用读写锁,因为读多写少。
  • 内存管理:订单量大的时候,队列可能占用大量内存。要设置队列上限,或者用内存池复用订单对象。

下面是一个简化的代码示例,展示如何实现综合排序:

// 价格优先 + 时间优先的撮合队列(简化版)
class OrderQueue {
    // 价格桶:key=价格,value=该价格下的订单列表(按时间排序)
    TreeMap<Double, LinkedList<Order>> buyBuckets;
    TreeMap<Double, LinkedList<Order>> sellBuckets;
    
    public void addOrder(Order order) {
        if (order.isBuy()) {
            // 买单:价格从高到低排序
            LinkedList<Order> bucket = buyBuckets.computeIfAbsent(
                order.price, k -> new LinkedList<>());
            // 时间优先:插入到链表尾部(假设时间递增)
            bucket.addLast(order);
        } else {
            // 卖单:价格从低到高排序
            LinkedList<Order> bucket = sellBuckets.computeIfAbsent(
                order.price, k -> new LinkedList<>());
            bucket.addLast(order);
        }
    }
    
    public Order getBestBuyOrder() {
        // 获取最高价买单的最早订单
        Map.Entry<Double, LinkedList<Order>> entry = buyBuckets.lastEntry();
        if (entry == null) return null;
        return entry.getValue().peekFirst();
    }
    
    public Order getBestSellOrder() {
        // 获取最低价卖单的最早订单
        Map.Entry<Double, LinkedList<Order>> entry = sellBuckets.firstEntry();
        if (entry == null) return null;
        return entry.getValue().peekFirst();
    }
}

我的建议:实际生产环境中,不要直接用LinkedList,它的内存开销大。我习惯用数组实现的循环队列,或者用Netty的Recycler来复用对象。另外,价格桶的TreeMap要小心,浮点数作为key会有精度问题。我一般把价格乘以10000转成整数来用。

最后,我想强调一点:价格优先和时间优先的组合,不仅仅是技术实现问题,更是公平性的体现。一个公平的撮合系统,能让所有参与者信任市场。如果你在设计中偷懒,比如用随机排序代替时间优先,短期看没问题,长期一定会出问题。

好了,这一章就到这里。记住:价格优先是骨架,时间优先是血肉,两者缺一不可。


专注资料整理