3. 数据存储与缓存:时序数据库选型与缓存策略
做量化交易系统,数据存储这块儿,我踩过的坑真不少。说白了,行情数据就是你的命根子。Tick 级别的数据,一天下来几个 GB 很正常。怎么存、怎么取、怎么保证回测和实盘都能跑得飞快?这章咱们好好聊聊。
3.1 时序数据库选型:TDengine vs TimescaleDB
我最早做量化系统时,用的是 MySQL 硬扛行情数据。结果呢?一张表几亿行,查询一个月的分钟 K 线要等十几秒。后来才转向时序数据库。目前主流的两款:TDengine 和 TimescaleDB。我分别说说我的体会。
3.1.1 TDengine:为物联网而生,也适合高频行情
TDengine 是涛思数据的产品。它的设计理念很纯粹——就是针对时序数据优化的。我项目中用过它来存储期货的 Tick 数据,效果不错。
- 写入性能极强:单机每秒能写入几百万个数据点。我实测过,比 InfluxDB 快 3-5 倍。
- 列式存储 + 压缩:默认压缩比能达到 10:1 甚至更高。我存了半年的 Tick 数据,原始 2TB,压缩后不到 200GB。
- 超级表概念:一张超级表可以管理成千上万个子表。比如每只股票一张子表,查询时自动路由。
3.1.2 TimescaleDB:PostgreSQL 的时序扩展
TimescaleDB 是建立在 PostgreSQL 之上的。如果你团队已经熟悉 PG,那它是个平滑的选择。我有个朋友在私募做中低频策略,就用的它。
- 全 SQL 支持:JOIN、窗口函数、子查询,随便用。TDengine 的 SQL 支持还在追赶中。
- 自动分区:按时间自动创建分区表,查询时自动剪枝。
- 压缩能力:列式压缩,但压缩比略低于 TDengine。
3.1.3 选型对比表
| 维度 | TDengine | TimescaleDB |
|---|---|---|
| 写入性能 | 极高(百万点/秒) | 高(十万点/秒) |
| 压缩比 | 10:1 ~ 20:1 | 5:1 ~ 10:1 |
| SQL 兼容性 | 部分支持 | 完全支持 |
| 部署复杂度 | 低(单机即可) | 中(依赖 PG) |
| 适用场景 | 高频、超高频行情 | 中低频、复杂查询 |
我个人习惯是:高频策略用 TDengine,中低频策略用 TimescaleDB。如果你团队 PG 经验丰富,也可以全用 TimescaleDB,但要做好写入性能的压测。
3.2 Redis 缓存策略:热数据预加载
时序数据库再快,也快不过内存。Redis 就是那个「快」的角色。我见过不少系统,查询行情时直接怼数据库,结果回测跑一天,实盘延迟几十毫秒。其实很多数据是可以预加载到 Redis 的。
3.2.1 什么是热数据?
热数据就是最近频繁访问的数据。比如:
- 最近 5 分钟的 Tick 数据
- 当日所有合约的 OHLCV
- 策略依赖的实时指标(如均线、布林带)
这些数据如果每次都去查 TDengine,延迟至少 1-2 毫秒。而 Redis 只需要 0.1 毫秒。你想想看,一个策略每秒计算 100 次,光数据读取就差了 100 毫秒,这差距可不小。
3.2.2 预加载策略
我常用的做法是:
- 启动时全量加载:系统启动时,把最近 N 天的行情数据从 TDengine 批量加载到 Redis。
- 实时增量更新:行情推送过来时,同时写入 TDengine 和 Redis。保证 Redis 里的数据是最新的。
- 过期淘汰:设置 TTL,比如 7 天。超过 7 天的数据自动淘汰,避免内存爆炸。
# 伪代码:启动时预加载热数据
def preload_hot_data(symbols, days=7):
redis_client = Redis(host='localhost', port=6379)
td_client = TDengine(host='localhost', port=6030)
for symbol in symbols:
# 从 TDengine 查询最近 7 天的分钟 K 线
data = td_client.query(
f"SELECT ts, open, high, low, close, volume "
f"FROM market.kline_1m "
f"WHERE symbol='{symbol}' "
f"AND ts >= NOW() - INTERVAL '{days} days'"
)
# 批量写入 Redis,使用有序集合存储
for row in data:
key = f"kline:1m:{symbol}"
score = row['ts'].timestamp()
value = json.dumps({
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
})
redis_client.zadd(key, {value: score})
# 设置过期时间
redis_client.expire(key, 60 * 60 * 24 * (days + 1))
print(f"预加载完成,共 {len(symbols)} 个合约")
3.3 数据分区与压缩:列式存储优化
数据量大了,查询慢是必然的。怎么优化?两个方向:分区和压缩。这两件事做好了,查询速度能提升 10 倍以上。
3.3.1 数据分区
分区就是把大表拆成小表。时序数据天然适合按时间分区。比如:
- 按天分区:每天一个分区。查询某一天的数据,只扫描一个分区。
- 按月分区:适合回测场景。回测一个月的数据,只加载一个分区。
- 按合约分区:每个合约独立分区。查询某只股票,只扫描对应分区。
在 TDengine 里,分区是自动的。你只需要建表时指定分区键:
-- TDengine 自动按天分区
CREATE TABLE market.tick (
ts TIMESTAMP,
symbol BINARY(10),
price DOUBLE,
volume INT
) TAGS (exchange BINARY(10))
PARTITION BY (ts) EVERY(1d);
在 TimescaleDB 里,需要手动创建分区(超表):
-- TimescaleDB 创建超表
CREATE TABLE market.tick (
ts TIMESTAMPTZ NOT NULL,
symbol TEXT,
price DOUBLE PRECISION,
volume INT
);
SELECT create_hypertable('market.tick', 'ts',
chunk_time_interval => INTERVAL '1 day');
3.3.2 列式存储与压缩
行式存储是按行存的,查询时即使只查一列,也要把整行读出来。列式存储则按列存,只读需要的列。这就是为什么列式存储对分析型查询更友好。
TDengine 和 TimescaleDB 都支持列式压缩。常用的压缩算法:
- Delta 编码:存差值而不是原始值。比如价格 100.01, 100.02, 100.01,存差值 0, +0.01, -0.01。
- RLE(游程编码):连续相同的数据只存一次。比如成交量 100, 100, 100,存 (100, 3)。
- Zstd:通用压缩算法,压缩比高,解压速度快。
3.4 知识体系结构图
下面这张图,是我梳理的数据存储与缓存的核心逻辑。你可以看到数据从行情源流入,经过 Redis 缓存,最终落地到时序数据库的全过程。
嗯,这张图基本概括了本章的核心。数据从行情源进来,先经过 Redis 缓存层,再落地到时序数据库。数据库内部通过分区和压缩来优化存储和查询。最终,策略引擎和回测系统从这两层读取数据,各取所需。
公众号:蓝海资料掘金营,微信deep3321