数据存储与检索:时序数据库选型与优化

做市商系统里,数据就是命根子。行情数据、订单簿快照、成交记录,每秒都在疯狂产生。我见过不少团队,策略写得漂亮,结果数据存得乱七八糟,回测跑不动,实盘查问题要翻半天日志。说白了,存储层没搞好,上层再牛也白搭。

这一章,咱们就聊聊时序数据库的选型、分区、压缩、查询优化和备份。我会结合我在做市商项目里的实际踩坑经验,给你讲清楚每个环节的要点。

为什么非要用时序数据库?

你想想看,做市商的数据有什么特点?

  • 写多读少:行情数据每秒几百上千条写入,但查询往往是事后分析
  • 时间有序:数据天然按时间到达,很少修改
  • 数据量大:一天就能产生几个GB甚至几十GB
  • 聚合查询多:经常要算分钟级、小时级的统计指标

传统的关系型数据库,比如MySQL,处理这种场景会很吃力。我早期一个项目就用MySQL存行情,结果写入成了瓶颈,查询还慢得要命。后来换了时序数据库,性能直接翻了几十倍。

InfluxDB vs ClickHouse:怎么选?

这两个是目前最主流的时序数据库方案。我两个都用过,各有千秋。

特性 InfluxDB ClickHouse
写入性能 极高,单机每秒百万级 极高,列式存储写入快
查询灵活性 有限,主要面向时序 强,支持标准SQL
数据压缩 好,自带压缩算法 极好,列式压缩比高
部署复杂度 简单,开箱即用 中等,需要调优
适用场景 纯时序数据,监控告警 时序+分析,复杂查询

我个人习惯:如果只是存行情数据,做简单的聚合查询,InfluxDB就够了。但如果你需要做复杂的回测分析,或者要关联订单数据,ClickHouse更合适。我在一个做市商项目里,用ClickHouse同时存了行情和订单数据,一条SQL就能算出某个时间段的盈亏,非常方便。

核心建议:小团队、快速原型用InfluxDB;大团队、复杂分析用ClickHouse。两个都学也不亏,毕竟技术栈越宽,解决问题的能力越强。

数据分区策略:别让查询扫全表

时序数据最大的特点就是时间范围查询。如果不做分区,查询一个月的数据可能要扫描整个表,那效率就太低了。

我建议按时间分区,具体粒度看你的数据量:

  • 按天分区:适合每天数据量在几GB以内的场景
  • 按小时分区:适合高频行情,每天数据量几十GB
  • 按月分区:适合低频数据,比如分钟级K线

在ClickHouse里,分区语法很简单:

CREATE TABLE trades (
    symbol String,
    price Float64,
    volume Float64,
    event_time DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (symbol, event_time);

嗯,这里要注意:分区不是越多越好。分区太多会导致元数据膨胀,反而影响性能。我见过有人按分钟分区,结果一个表几万个分区,查询反而变慢了。一般来说,每个分区在100MB到1GB之间比较合理。

避坑指南:我曾经在一个项目里,分区粒度设得太细,结果数据量上来后,分区数暴增,导致合并线程忙不过来。后来改成按天分区,问题就解决了。记住,分区是给查询用的,不是给存储用的。

数据压缩:省的不只是空间

压缩的好处很明显:节省磁盘空间,减少I/O,查询更快。但不同的压缩算法,效果差异很大。

时序数据有个特点:相邻时间点的数据往往很接近。比如价格,上一秒和下一秒可能只差几个tick。利用这个特点,压缩率可以非常高。

ClickHouse支持多种压缩算法:

  • LZ4:速度快,压缩比中等,适合高频写入场景
  • ZSTD:压缩比高,速度稍慢,适合冷数据
  • Delta + LZ4:先做差值编码,再压缩,时序数据效果极好

我一般这样配置:

CREATE TABLE trades (
    symbol String CODEC(ZSTD),
    price Float64 CODEC(Delta, LZ4),
    volume Float64 CODEC(Delta, LZ4),
    event_time DateTime CODEC(DoubleDelta, LZ4)
) ENGINE = MergeTree()
...;

你看,价格和成交量用了Delta编码,因为它们是连续变化的。时间戳用了DoubleDelta,因为时间间隔基本固定。这样配置下来,压缩比能达到10:1以上。我有个项目,原始数据100GB,压缩后不到10GB。

注意:压缩不是越强越好。ZSTD虽然压缩比高,但写入和查询时CPU开销大。如果你的系统CPU资源紧张,用LZ4更稳妥。我建议在写入路径上用LZ4,在归档路径上用ZSTD。

查询优化:让SQL飞起来

数据存好了,查不出来也是白搭。做市商系统里,最常见的查询就是「某只币种在某个时间段的行情」或者「某个策略的盈亏统计」。

几个优化技巧:

  • 用好排序键:ClickHouse的ORDER BY决定了数据的物理存储顺序。把最常用的查询条件放在前面,比如(symbol, event_time)。这样查询指定币种和时间范围时,能快速定位到数据块。
  • 避免SELECT *:只查需要的列。时序数据列很多,但每次查询可能只需要价格和成交量。少查一列,就少读很多数据。
  • 利用物化视图:对于常用的聚合查询,比如每分钟的平均价格,可以建物化视图,提前算好。查询时直接读结果,不用再扫描原始数据。

举个例子:

-- 创建物化视图,每分钟聚合一次
CREATE MATERIALIZED VIEW trades_minute
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMMDD(minute)
ORDER BY (symbol, minute)
AS SELECT
    symbol,
    toStartOfMinute(event_time) AS minute,
    avg(price) AS avg_price,
    sum(volume) AS total_volume
FROM trades
GROUP BY symbol, minute;

这样,查询分钟级K线时,直接查trades_minute表,速度能快几十倍。我在项目里就是这么干的,原来查询要3秒,优化后不到50毫秒。

小技巧:查询时尽量用精确的时间范围。比如「2024-01-01 10:00:00 到 2024-01-01 10:05:00」,而不是「最近5分钟」。精确的时间范围能让数据库直接定位到分区,避免扫描不必要的数据。

数据备份:别等丢了才后悔

做市商的数据有多重要?丢了就是真金白银的损失。我见过有人硬盘坏了,一周的行情数据全没了,回测结果对不上,策略参数也调不了。那叫一个惨。

备份策略我建议这样:

  • 全量备份:每周一次,保存到对象存储(比如S3)
  • 增量备份:每天一次,只备份当天的数据
  • 实时备份:通过数据复制,保持两个节点数据一致

ClickHouse的备份命令:

# 全量备份
clickhouse-backup create full_backup_20240101

# 增量备份
clickhouse-backup create incremental_backup_20240102 --base full_backup_20240101

# 恢复到指定时间点
clickhouse-backup restore full_backup_20240101

InfluxDB的备份更简单:

# 备份所有数据
influxd backup -portable /path/to/backup

# 恢复
influxd restore -portable /path/to/backup

血的教训:我曾经只做了本地备份,结果机房断电,硬盘全挂了。从那以后,我坚持「两地三中心」的备份策略:本地一份,同城另一台机器一份,云端再一份。虽然成本高了点,但心里踏实。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从数据写入到查询,每个环节都有对应的优化手段。

时序数据库核心架构 行情数据流 数据库选型:InfluxDB / ClickHouse 核心优化:分区策略 + 数据压缩 + 查询优化 + 备份 分区策略 按天/小时分区 数据压缩 LZ4 / ZSTD / Delta 查询优化 排序键 / 物化视图 数据备份 全量+增量+实时 稳定、高效的数据存储层

说白了,数据存储这件事,没有银弹。选型要看场景,优化要结合实际。但核心思路是不变的:理解数据特点,针对性设计。你只要把分区、压缩、查询、备份这四个环节都做到位,数据层就不会成为系统的瓶颈。

好了,这一章就到这里。记住,数据是做市商系统的基石,花时间把存储层搞好,后面会省很多事。

专注资料整理