第1章:内存计算引擎——为什么做市系统离不开它?

做市系统里,什么最要命?

延迟。一毫秒的延迟,可能就是几万块的亏损。我见过太多团队,数据库用MySQL,Redis当缓存用,结果行情一波动,系统直接卡死。说白了,做市商的核心竞争力,就是比别人快那么一点点。

内存计算引擎,就是解决这个问题的。今天咱们聊聊内存数据库选型、哈希表与跳表的应用、缓存策略设计,还有读写分离架构。这些都是我这些年踩坑踩出来的经验。

4.1 内存数据库选型:Redis vs 自研In-Memory

选Redis还是自己写内存存储?这个问题,我当年也纠结过。

对比维度 Redis 自研In-Memory
开发成本 低,开箱即用 高,需要从零搭建
性能 单线程,10万+ QPS 可定制,轻松百万级
数据结构 丰富(String/Hash/ZSet等) 按需实现,更轻量
持久化 RDB/AOF,但影响性能 通常不做持久化
典型场景 订单簿快照、行情缓存 高频订单簿、Tick级计算

我个人习惯是:订单簿的实时计算用自研内存引擎,行情缓存和辅助数据用Redis。为什么?因为Redis的ZSet虽然支持跳表,但它的操作是网络IO,再快也有开销。而自研引擎直接内嵌在进程里,零拷贝访问。

核心原则:做市系统的内存引擎,延迟必须控制在微秒级。Redis适合做"辅助存储",不适合做"核心计算"。别把Redis当万能药。

4.2 哈希表与跳表:订单簿的左右手

做市系统里,订单簿是最核心的数据结构。买盘和卖盘,本质上就是两个有序列表。

哈希表用来做O(1)的随机查找。比如根据订单ID快速找到订单详情。我在项目中遇到过一个问题:订单量大了以后,哈希冲突导致性能下降。解决方案很简单——用开放寻址法,别用链地址法。链地址法在内存里跳来跳去,缓存不友好。

跳表用来做范围查询和有序遍历。比如查询卖一价到卖十价的所有订单。跳表的平均复杂度是O(log n),比红黑树实现简单,而且支持并发操作。

我的经验:跳表的层数不要设太高。默认16层就够了。我曾经设了32层,结果内存占用翻倍,性能反而没提升。跳表的核心是"概率平衡",不是"绝对平衡"。

// 跳表节点定义(C++)
struct SkipListNode {
    int64_t price;          // 价格
    int64_t volume;         // 数量
    int64_t order_id;       // 订单ID
    std::vector<SkipListNode*> forward;  // 前进指针数组
    
    SkipListNode(int level) : forward(level, nullptr) {}
};

// 跳表插入(简化版)
void insert(int64_t price, int64_t volume) {
    int level = randomLevel();  // 随机生成层数
    SkipListNode* newNode = new SkipListNode(level);
    newNode->price = price;
    newNode->volume = volume;
    
    // 查找插入位置
    SkipListNode* current = head;
    for (int i = level - 1; i >= 0; --i) {
        while (current->forward[i] && 
               current->forward[i]->price < price) {
            current = current->forward[i];
        }
        newNode->forward[i] = current->forward[i];
        current->forward[i] = newNode;
    }
}

4.3 缓存策略设计:别让数据"冷"下来

做市系统的缓存,和Web应用的缓存完全是两码事。Web缓存可以容忍几秒的过期,做市系统不行。一秒钟的过期,可能就错过了最佳报价时机。

我常用的缓存策略有三种:

  1. LRU(最近最少使用):适合缓存订单簿的快照。最近被访问的订单,大概率还会被访问。
  2. TTL(生存时间):适合缓存行情数据。比如1秒过期,过期后重新拉取。
  3. Write-Through(写穿透):适合缓存订单状态。每次写入都同步更新缓存和数据库。

避坑指南:我曾经在缓存里存了完整的订单簿,结果每次行情更新都要全量替换,性能惨不忍睹。后来改成只缓存"增量更新"——只存变化的订单,查询时再合并。性能提升了10倍。

还有一个细节:缓存预热。系统启动时,别等用户请求才加载数据。提前把热数据加载到内存里。我一般会在系统启动后,先加载最近1000笔成交记录和当前订单簿快照。

4.4 读写分离架构:让计算和查询互不干扰

做市系统里,写操作是高频的——每笔成交、每个报价都要写。读操作也是高频的——策略引擎要读订单簿、风控要读持仓。如果读写混在一起,锁竞争会拖垮性能。

读写分离架构,说白了就是:写主库,读从库。但做市系统的读写分离,和数据库的读写分离不太一样。

我设计的架构是这样的:

  • 写引擎:负责接收行情、更新订单簿、生成报价。数据直接写入内存中的主表。
  • 读引擎:负责给策略引擎、风控系统提供查询服务。数据来自内存中的从表。
  • 同步机制:主表更新后,通过无锁队列(Lock-Free Queue)异步同步到从表。

这样做的好处是:写操作不会被读操作阻塞。读操作也不会因为写操作而等待锁。

关键点:同步延迟必须控制在微秒级。我用的方法是"内存拷贝+原子指针切换"。主表更新后,拷贝一份新数据,然后原子地切换指针。读线程永远读到的是完整快照。

// 读写分离核心实现(伪代码)
class OrderBook {
    std::atomic<Snapshot*> m_snapshot;  // 原子指针
    
    void update(const Order& order) {
        Snapshot* new_snap = new Snapshot(*m_snapshot.load());
        new_snap->apply(order);  // 应用增量更新
        m_snapshot.store(new_snap);  // 原子切换
    }
    
    Snapshot* read() {
        return m_snapshot.load();  // 无锁读取
    }
};

4.5 架构全景图

下面这张图,是我做市系统内存计算引擎的完整架构。你可以看到数据是怎么流转的:

做市库存实时计算引擎架构 行情数据源 交易所Tick流 写引擎(主表) 订单簿更新/报价生成 无锁同步队列 Lock-Free Queue 读引擎(从表) 原子指针切换 缓存层(Redis) 行情快照/辅助数据 策略引擎 做市/套利策略 风控系统 持仓/风险监控 持久化存储 MySQL/ClickHouse 监控告警 延迟/吞吐/错误率 图例: 数据源 写引擎 读引擎 缓存 策略 风控 持久化 监控

这张图里,数据从行情源进来,经过写引擎更新主表,然后通过无锁队列同步到从表。策略引擎和风控系统都从从表读数据,互不干扰。Redis缓存层用来存一些辅助数据,比如历史波动率、资金费率等。

一个小技巧:无锁队列的容量要设得足够大。我一般设成10万条,防止行情爆发时队列满了导致丢数据。如果队列满了,宁可丢弃旧数据,也不能阻塞写引擎。

好了,这一章的内容就到这里。内存计算引擎是做市系统的基石,选对工具、设计好架构,后面的事情就顺了。下一章咱们聊聊订单簿的实时更新算法,那才是真正考验性能的地方。


公众号:蓝海数据掘金营,微信deep3321