第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缓存可以容忍几秒的过期,做市系统不行。一秒钟的过期,可能就错过了最佳报价时机。
我常用的缓存策略有三种:
- LRU(最近最少使用):适合缓存订单簿的快照。最近被访问的订单,大概率还会被访问。
- TTL(生存时间):适合缓存行情数据。比如1秒过期,过期后重新拉取。
- 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 架构全景图
下面这张图,是我做市系统内存计算引擎的完整架构。你可以看到数据是怎么流转的:
这张图里,数据从行情源进来,经过写引擎更新主表,然后通过无锁队列同步到从表。策略引擎和风控系统都从从表读数据,互不干扰。Redis缓存层用来存一些辅助数据,比如历史波动率、资金费率等。
一个小技巧:无锁队列的容量要设得足够大。我一般设成10万条,防止行情爆发时队列满了导致丢数据。如果队列满了,宁可丢弃旧数据,也不能阻塞写引擎。
好了,这一章的内容就到这里。内存计算引擎是做市系统的基石,选对工具、设计好架构,后面的事情就顺了。下一章咱们聊聊订单簿的实时更新算法,那才是真正考验性能的地方。
公众号:蓝海数据掘金营,微信deep3321