22、数据库设计与优化:关系型数据库、NoSQL选型、读写分离、分库分表
做量化系统,数据库这块儿,说实话,是很多人的噩梦。
结构化产品做市,数据量不是最大的,但实时性要求极高,而且数据一致性绝对不能出问题。你想想看,一个订单簿的快照写错了,或者一笔成交记录丢了,那可不是闹着玩的。
我个人习惯,在设计数据库架构之前,先问自己三个问题:
- 这笔数据,丢了能忍吗?
- 这笔数据,晚几秒读到能忍吗?
- 这笔数据,会膨胀到多大?
这三个问题想清楚,选型就完成了一半。
关系型数据库:压舱石
做市系统里,账户、持仓、成交、风控参数,这些核心资产,我建议全部放在关系型数据库里。为什么?因为ACID。
我在项目中遇到过,有人把持仓数据放Redis,结果宕机后重建,对账对了一整晚。嗯,从那以后,凡是涉及「钱」的数据,我都用PostgreSQL或者MySQL。
选哪个?我个人更倾向PostgreSQL。原因很简单:
- 它支持JSONB,可以灵活存储一些半结构化的风控配置。
- 它的窗口函数和CTE,做复杂查询时,写起来很舒服。
- 它的MVCC实现,在高并发读写下,性能衰减比MySQL平滑。
当然,MySQL生态更成熟,运维成本更低。如果你团队里DBA是MySQL专家,用MySQL也没问题。
核心原则: 关系型数据库只存「状态」和「流水」,不存「快照」和「日志」。
NoSQL选型:各司其职
做市系统里,有些数据天生就不适合放在关系型数据库里。比如:
- 行情数据:每秒几千笔tick,写入量巨大,但几乎不更新。
- 订单簿快照:需要快速写入和读取,但不需要复杂查询。
- 会话缓存:临时数据,丢了也无所谓。
这时候,NoSQL就派上用场了。
| 数据类型 | 推荐方案 | 理由 |
|---|---|---|
| 实时行情tick | InfluxDB / TimescaleDB | 时序数据库,写入吞吐高,自动降采样 |
| 订单簿快照 | Redis | 纯内存,读写延迟<1ms,支持过期策略 |
| 会话/临时数据 | Redis | TTL自动清理,不用操心 |
| 历史成交明细 | Elasticsearch | 全文检索,方便复盘和审计 |
这里有个坑,我踩过。曾经把订单簿快照直接存MySQL,结果写入压力一大,主库的CPU直接飙到100%。后来改成Redis,问题瞬间解决。说白了,选对工具,比优化SQL更重要。
读写分离:读多写少的解药
做市系统里,读和写的比例,有时候能达到10:1。风控查询、报表统计、实时监控,全是读操作。而写操作,只有成交和撤单。
这时候,读写分离就很有必要了。
我建议的架构是:
- 主库:只处理写操作。INSERT、UPDATE、DELETE。
- 从库:只处理读操作。SELECT。
- 中间层:用ProxySQL或MaxScale,自动路由SQL。
避坑指南: 我曾经在从库上跑了一个复杂的风控聚合查询,结果从库延迟了3秒。主库的数据已经变了,但风控还在用旧数据判断。差点导致一笔超限交易。
从那以后,我定了一条铁律:所有涉及风控的读操作,必须走主库。
实现读写分离,代码层面其实很简单。以Python为例:
# 配置读写分离
DATABASES = {
'default': { # 主库
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'market_maker',
'HOST': '192.168.1.10',
},
'readonly': { # 从库
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'market_maker',
'HOST': '192.168.1.11',
}
}
# 手动路由
def get_db_for_read(model):
if model._meta.model_name in ['RiskControl', 'Position']:
return 'default' # 风控和持仓走主库
return 'readonly' # 其他走从库
分库分表:当单库扛不住的时候
做市系统做到一定规模,单库肯定扛不住。我见过一个极端案例:某家做市商,一天成交了200万笔,单表数据量到了5亿行。查询一次历史成交,要等30秒。
这时候,必须分库分表。
分库分表的核心,是选对分片键。我建议用交易对+日期作为分片键。为什么?
- 查询时,99%的场景都带着交易对和时间范围。
- 数据可以按时间归档,冷热分离。
警告: 千万不要用「自增ID」作为分片键。否则新数据全往一个库写,热点问题会让你崩溃。
下面是一个简单的分表策略:
-- 按交易对和日期分表
CREATE TABLE trades_btcusdt_20250101 (
id BIGSERIAL PRIMARY KEY,
trade_time TIMESTAMP,
price NUMERIC(20,8),
volume NUMERIC(20,8),
side VARCHAR(4)
);
CREATE TABLE trades_btcusdt_20250102 (
-- 结构同上
);
CREATE TABLE trades_ethusdt_20250101 (
-- 结构同上
);
当然,手动管理这么多表不现实。我建议用ShardingSphere或Vitess这类中间件。它们可以自动路由SQL,你写代码时,感觉就像在操作一张表。
一张图看懂数据库架构
下面这张图,是我做过的做市系统数据库架构。你可以参考一下:
优化实战:从慢查询到毫秒级
最后,分享一个我优化数据库的实战案例。
有一次,做市系统的持仓查询接口,响应时间从50ms涨到了2秒。查了一下,发现是SQL没走索引。
原SQL是这样的:
SELECT * FROM positions
WHERE account_id = 12345
AND trade_date BETWEEN '2025-01-01' AND '2025-01-31';
问题出在trade_date字段上。虽然建了索引,但BETWEEN查询导致索引失效,走了全表扫描。
优化方案:
- 把
trade_date改成trade_month(按月分区)。 - 加上
account_id + trade_month的联合索引。
优化后,查询时间降到了30ms。嗯,有时候,一个索引就能解决大问题。
我的习惯: 每次上线前,我都会用EXPLAIN ANALYZE跑一遍所有核心SQL。看到「Seq Scan」就亮红灯,必须改成「Index Scan」才放行。
数据库设计,说白了就是权衡的艺术。没有银弹,只有最适合你业务场景的方案。多踩坑,多复盘,慢慢就有感觉了。
交易系统化学习资料 微信Strategy888888