第九章 数据库选型与设计:时序数据库、关系型数据库与缓存策略
做量化交易系统,数据库选型是个绕不开的坎。我见过不少团队,一开始随便选个MySQL存行情数据,结果几个月后查询慢得像蜗牛爬。说白了,不同的数据有不同的脾气,你得顺着它来。
今天我们就聊聊衍生品做市商系统里,怎么搭配使用三种数据库:时序数据库、关系型数据库和缓存。嗯,这里要注意,不是选一个就完事了,而是让它们各司其职。
9.1 数据分类与存储需求
先理清我们到底要存什么数据。我习惯把数据分成三类:
- 时序数据:行情快照、逐笔成交、订单簿快照。特点是写入频繁、查询按时间范围、几乎不修改。
- 关系数据:账户信息、订单记录、成交明细、风控规则。特点是结构化、需要事务支持、经常关联查询。
- 缓存数据:实时持仓、最新价格、限价单状态。特点是读写极快、允许短暂不一致、数据量小。
你想想看,如果把行情数据硬塞进关系型数据库,每次插入都要维护索引,写入性能直接崩掉。反过来,用时序数据库存订单记录,那查询关联数据时又得写一堆奇怪的SQL。
核心原则:让合适的数据库处理合适的数据。时序数据进时序库,关系数据进关系库,热数据进缓存。
9.2 时序数据库:InfluxDB vs ClickHouse
时序数据库这块,我主要用过InfluxDB和ClickHouse。两者都能处理海量时序数据,但设计理念完全不同。
9.2.1 InfluxDB:专为时序而生
InfluxDB的设计哲学就是「时序优先」。它的数据模型很简单:measurement(表)、tag(标签)、field(字段)、time(时间戳)。
我在项目中遇到过一个问题:刚开始用InfluxDB存逐笔成交数据,每秒写入几万条,查询最近1分钟的成交分布,响应时间都在10ms以内。但后来数据量到了TB级别,聚合查询开始变慢。
InfluxDB的强项是写入和简单查询。它的压缩率很高,磁盘占用比MySQL少5-10倍。但它的SQL-like查询语言(Flux)学习曲线有点陡,而且不支持JOIN。
-- InfluxDB写入示例(使用Flux)
from(bucket: "tick_data")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "trade" and r.symbol == "BTC-USDT")
|> aggregateWindow(every: 1m, fn: mean)
|> yield(name: "mean_price")
9.2.2 ClickHouse:列式存储的猛兽
ClickHouse是列式存储数据库,严格来说不是纯粹的时序数据库,但做时序分析非常强。它的核心优势是:
- 极致的查询性能:列式存储+向量化执行,聚合查询比InfluxDB快一个数量级
- 标准SQL支持:团队上手成本低,可以JOIN、子查询
- 高压缩比:我见过行情数据压缩到原始大小的1/10
但ClickHouse也有短板:写入是批量模式,不适合高频逐条写入。而且它的更新和删除操作很重,不适合频繁修改的数据。
-- ClickHouse创建时序表
CREATE TABLE trades (
symbol String,
price Float64,
volume Float64,
trade_time DateTime,
side String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(trade_time)
ORDER BY (symbol, trade_time);
我的建议:如果团队SQL功底好,优先选ClickHouse。如果追求极简运维和快速上手,InfluxDB更合适。我个人在主力系统里用ClickHouse,辅助监控用InfluxDB。
9.3 关系型数据库:PostgreSQL
关系型数据库这块,我几乎只用PostgreSQL。为什么?因为它够稳,功能够全,而且开源。
做市商系统里,订单和成交数据需要严格的事务保证。比如一个订单被部分成交,你需要原子性地更新订单状态和插入成交记录。PostgreSQL的ACID特性在这里是刚需。
我记得有一次排查一个bug,发现订单状态和成交记录对不上。后来用PostgreSQL的SERIALIZABLE隔离级别,配合SELECT ... FOR UPDATE,才彻底解决了并发问题。
-- PostgreSQL订单表设计
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
side SMALLINT NOT NULL, -- 1: buy, -1: sell
price NUMERIC(20, 8) NOT NULL,
volume NUMERIC(20, 8) NOT NULL,
filled_volume NUMERIC(20, 8) DEFAULT 0,
status SMALLINT DEFAULT 0, -- 0: pending, 1: filled, 2: cancelled
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建索引加速查询
CREATE INDEX idx_orders_symbol_status ON orders(symbol, status);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
PostgreSQL还有一个杀手锏:分区表。订单数据量大了之后,按时间分区可以显著提升查询性能。我习惯按月分区,历史数据自动归档。
注意:不要用PostgreSQL存高频行情数据。我见过有人把tick数据塞进PG,结果写入TPS上不去,查询也慢。时序数据就该交给时序库。
9.4 Redis缓存策略
缓存层我用Redis,这个没什么好争议的。关键是缓存什么、怎么缓存、过期策略怎么定。
9.4.1 缓存什么
我总结了三类必须缓存的数据:
- 实时行情:最新买卖价、最新成交价。延迟要求毫秒级。
- 账户持仓:当前持仓数量、可用余额。每次下单前都要查。
- 限价单状态:未成交订单的ID和价格。用于快速撤单和风控。
9.4.2 缓存策略
Redis的缓存策略,说白了就是「快」和「准」的平衡。我常用的几种模式:
# 1. 最新行情:使用String类型,设置过期时间
SET market:BTC-USDT:last_price "45000.50" EX 1 # 1秒过期
# 2. 订单簿快照:使用Hash类型
HSET orderbook:BTC-USDT bids "45000.00" "1.5" "44999.50" "2.0"
HSET orderbook:BTC-USDT asks "45001.00" "0.8" "45002.00" "1.2"
# 3. 账户持仓:使用Hash,持久化但设置过期
HSET account:user123:positions BTC-USDT "{\"total\": 10, \"available\": 8}"
EXPIRE account:user123:positions 3600 # 1小时过期,自动刷新
避坑指南:我曾经犯过一个错误——把Redis当数据库用,所有数据都不设过期时间。结果内存爆了,系统直接挂掉。记住,Redis是缓存,不是持久化存储。重要数据一定要落盘到PostgreSQL或ClickHouse。
9.4.3 缓存一致性
缓存和数据库的数据不一致,是交易系统的噩梦。我常用的策略是:
- 先更新数据库,再删除缓存:保证最终一致性
- 设置合理的过期时间:即使缓存没更新,过期后也会重新加载
- 使用Redis的Pub/Sub:数据变更时广播通知,所有实例同步更新
9.5 整体架构与数据流
下面这张图是我在项目中实际使用的数据库架构。你可以看到数据是怎么流转的:
这个架构的核心思路是:数据从行情源进来,经过消息队列削峰填谷,然后并行写入三个存储层。交易引擎读数据时,优先走Redis缓存,缓存没有再去查PostgreSQL或ClickHouse。
9.6 选型总结
最后,我整理了一个对比表格,方便你快速决策:
| 特性 | InfluxDB | ClickHouse | PostgreSQL | Redis |
|---|---|---|---|---|
| 数据模型 | 时序专用 | 列式存储 | 关系型 | 键值对 |
| 写入性能 | 极高(单机百万/秒) | 高(批量写入) | 中等 | 极高(内存操作) |
| 查询性能 | 时序查询快 | 聚合查询极快 | 关联查询强 | 单键查询极快 |
| 事务支持 | 不支持 | 有限支持 | 完整ACID | 有限支持 |
| 适用场景 | 行情快照、监控 | 历史分析、报表 | 订单、账户、风控 | 实时数据、会话 |
| 运维复杂度 | 低 | 中 | 中 | 低 |
一句话总结:行情数据进ClickHouse,订单账户进PostgreSQL,热数据进Redis。三者配合,才能支撑起一个高性能的做市商系统。
嗯,数据库选型这块就聊这么多。记住,没有银弹,只有最适合你场景的组合。我在项目中踩过的坑,希望你能避开。
无相订单流研究社 微信Lucian808555