22、数据库设计与优化:关系型数据库、NoSQL选型、读写分离、分库分表

做量化系统,数据库这块儿,说实话,是很多人的噩梦。

结构化产品做市,数据量不是最大的,但实时性要求极高,而且数据一致性绝对不能出问题。你想想看,一个订单簿的快照写错了,或者一笔成交记录丢了,那可不是闹着玩的。

我个人习惯,在设计数据库架构之前,先问自己三个问题:

  • 这笔数据,丢了能忍吗?
  • 这笔数据,晚几秒读到能忍吗?
  • 这笔数据,会膨胀到多大?

这三个问题想清楚,选型就完成了一半。

关系型数据库:压舱石

做市系统里,账户、持仓、成交、风控参数,这些核心资产,我建议全部放在关系型数据库里。为什么?因为ACID。

我在项目中遇到过,有人把持仓数据放Redis,结果宕机后重建,对账对了一整晚。嗯,从那以后,凡是涉及「钱」的数据,我都用PostgreSQL或者MySQL。

选哪个?我个人更倾向PostgreSQL。原因很简单:

  • 它支持JSONB,可以灵活存储一些半结构化的风控配置。
  • 它的窗口函数CTE,做复杂查询时,写起来很舒服。
  • 它的MVCC实现,在高并发读写下,性能衰减比MySQL平滑。

当然,MySQL生态更成熟,运维成本更低。如果你团队里DBA是MySQL专家,用MySQL也没问题。

核心原则: 关系型数据库只存「状态」和「流水」,不存「快照」和「日志」。

NoSQL选型:各司其职

做市系统里,有些数据天生就不适合放在关系型数据库里。比如:

  • 行情数据:每秒几千笔tick,写入量巨大,但几乎不更新。
  • 订单簿快照:需要快速写入和读取,但不需要复杂查询。
  • 会话缓存:临时数据,丢了也无所谓。

这时候,NoSQL就派上用场了。

数据类型推荐方案理由
实时行情tickInfluxDB / TimescaleDB时序数据库,写入吞吐高,自动降采样
订单簿快照Redis纯内存,读写延迟<1ms,支持过期策略
会话/临时数据RedisTTL自动清理,不用操心
历史成交明细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 (
    -- 结构同上
);

当然,手动管理这么多表不现实。我建议用ShardingSphereVitess这类中间件。它们可以自动路由SQL,你写代码时,感觉就像在操作一张表。

一张图看懂数据库架构

下面这张图,是我做过的做市系统数据库架构。你可以参考一下:

做市系统数据库架构 交易引擎 读写分离中间件 (ProxySQL) 主库 (PostgreSQL) 账户/持仓/风控 从库1 历史成交查询 从库2 报表统计 Redis 订单簿快照/缓存 InfluxDB 实时行情tick Elasticsearch 历史成交检索 写入走主库,查询走从库。核心数据走关系型,非核心走NoSQL。

优化实战:从慢查询到毫秒级

最后,分享一个我优化数据库的实战案例。

有一次,做市系统的持仓查询接口,响应时间从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」才放行。

数据库设计,说白了就是权衡的艺术。没有银弹,只有最适合你业务场景的方案。多踩坑,多复盘,慢慢就有感觉了。


无相订单流研究社 微信Lucian808555