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。
  • 超级表概念:一张超级表可以管理成千上万个子表。比如每只股票一张子表,查询时自动路由。
我的经验:如果你主要做国内期货、股票的高频策略,TDengine 的部署和维护成本更低。它原生支持 SQL,团队上手快。

3.1.2 TimescaleDB:PostgreSQL 的时序扩展

TimescaleDB 是建立在 PostgreSQL 之上的。如果你团队已经熟悉 PG,那它是个平滑的选择。我有个朋友在私募做中低频策略,就用的它。

  • 全 SQL 支持:JOIN、窗口函数、子查询,随便用。TDengine 的 SQL 支持还在追赶中。
  • 自动分区:按时间自动创建分区表,查询时自动剪枝。
  • 压缩能力:列式压缩,但压缩比略低于 TDengine。
注意:TimescaleDB 的写入性能在高并发场景下不如 TDengine。如果你每秒要写入几十万条 Tick 数据,TimescaleDB 可能会成为瓶颈。

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 预加载策略

我常用的做法是:

  1. 启动时全量加载:系统启动时,把最近 N 天的行情数据从 TDengine 批量加载到 Redis。
  2. 实时增量更新:行情推送过来时,同时写入 TDengine 和 Redis。保证 Redis 里的数据是最新的。
  3. 过期淘汰:设置 TTL,比如 7 天。超过 7 天的数据自动淘汰,避免内存爆炸。
代码示例:Redis 预加载核心逻辑
# 伪代码:启动时预加载热数据
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)} 个合约")
避坑指南:我曾经犯过一个错误——把所有历史数据都加载到 Redis。结果内存直接爆了。后来才明白,Redis 只存热数据,冷数据交给 TDengine。内存再大也有限,别想着全量加载。

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:通用压缩算法,压缩比高,解压速度快。
我的建议:不要为了压缩而压缩。压缩比和查询性能需要平衡。我一般用 Zstd 级别 3,压缩比 8:1 左右,解压速度还能接受。

3.4 知识体系结构图

下面这张图,是我梳理的数据存储与缓存的核心逻辑。你可以看到数据从行情源流入,经过 Redis 缓存,最终落地到时序数据库的全过程。

量化交易系统 - 数据存储与缓存架构 行情数据源 Redis 缓存层 热数据预加载 · TTL 过期 · 实时增量 时序数据库 TDengine / TimescaleDB · 列式存储 · 分区 按时间/合约分区 列式压缩 (Zstd) 策略引擎 · 回测系统 · 实时监控

嗯,这张图基本概括了本章的核心。数据从行情源进来,先经过 Redis 缓存层,再落地到时序数据库。数据库内部通过分区和压缩来优化存储和查询。最终,策略引擎和回测系统从这两层读取数据,各取所需。

总结一下:选型没有标准答案。高频策略优先考虑 TDengine 的写入性能,中低频策略可以选 TimescaleDB 的 SQL 灵活性。Redis 只存热数据,别贪心。分区和压缩是必选项,不是可选项。

公众号:蓝海资料掘金营,微信deep3321