3. 金融行情数据模型
在构建 Kafka 金融行情高吞吐管道之前,必须先厘清“管道里到底流的是什么”。行情数据模型决定了消息的键设计、分区策略、序列化格式、压缩方式以及下游消费语义。如果模型理解偏差,后续无论 Kafka 参数怎么调优,都会在正确性上翻车。本节从交易所原始数据出发,梳理 Level-1、Level-2、逐笔委托、逐笔成交、快照与增量这几类核心结构,并给出它们在 Kafka 主题中的建模建议。
一、核心原理:从“状态”与“事件”两个视角看行情
所有行情数据本质上可以归为两类:状态类与事件类。状态类描述某一时刻市场的完整切片,典型代表是快照(Snapshot);事件类描述状态发生的变化,典型代表是逐笔委托、逐笔成交和增量更新(Incremental)。Level-1 与 Level-2 则是按信息深度划分的层级:Level-1 通常只包含最优一档买卖价量,Level-2 则包含多档盘口乃至全部委托队列。
理解这一点的意义在于:快照是幂等的,重复消费同一快照不会破坏状态;而逐笔事件是顺序敏感的,乱序或丢失会导致重建出的订单簿与真实市场不一致。因此,在 Kafka 中,快照类主题可以容忍更高的重试与重复,而逐笔类主题必须保证分区内严格有序,并谨慎设置 enable.idempotence 与 max.in.flight.requests.per.connection。
二、方法流程:各类数据结构的字段与建模要点
下面按数据类别逐一拆解关键字段与 Kafka 建模注意事项。
- Level-1 行情:包含最新价、成交量、成交额、最优买一/卖一价量。字段少、频率低,适合作为轻量级主题,供监控与简单策略使用。
- Level-2 行情:包含多档买卖盘(如十档),每档含价格与挂单量。数据量显著增大,需关注单条消息大小与压缩。
- 逐笔委托:记录每一笔新增、撤单委托,含委托编号、方向、价格、数量、委托类型。是重建订单簿的原始输入。
- 逐笔成交:记录每一笔撮合成交,含成交编号、买卖委托号、价格、数量、成交方向。用于计算真实成交与主动买卖。
- 快照:某一时刻的完整盘口切片,通常按固定频率(如每 3 秒)发布,用于快速对齐状态。
- 增量:仅包含发生变化的档位,需配合序号做连续性校验,否则会静默丢失状态。
在序列化选择上,推荐使用 Avro 或 Protobuf 而非 JSON,原因有三:一是字段固定、schema 可演进;二是二进制体积小,降低网络与磁盘压力;三是可与 Schema Registry 集成,避免上下游字段漂移。以下是一个逐笔成交的 Avro schema 片段示例:
{
"type": "record",
"name": "TradeTick",
"fields": [
{"name": "symbol", "type": "string"},
{"name": "tradeSeq", "type": "long"},
{"name": "exchangeTs","type": "long"},
{"name": "price", "type": "double"},
{"name": "volume", "type": "long"},
{"name": "side", "type": {"type": "enum", "name": "Side", "symbols": ["BUY","SELL","UNKNOWN"]}},
{"name": "bidOrderId","type": ["null","string"], "default": null},
{"name": "askOrderId","type": ["null","string"], "default": null}
]
}
三、案例场景:订单簿重建与快照对齐
假设我们要为某只股票维护实时订单簿。典型流程是:先消费一条快照作为初始状态,然后按序消费逐笔委托与逐笔成交,逐条应用到订单簿上。若中途发现增量序号不连续,则必须暂停应用并重新拉取快照对齐,否则订单簿会逐渐偏离真实市场。
在 Kafka 中,这一流程对应两个主题:market.snapshot 与 market.order.tick。快照主题使用 cleanup.policy=compact,保证每个 symbol 只保留最新快照;逐笔主题使用 cleanup.policy=delete 并设置合理的 retention.ms,保证可回放。消费端需维护每个 symbol 的 lastAppliedSeq,并在检测到跳号时触发重新对齐逻辑。
exchangeTs 与 recvTs。前者用于业务逻辑,后者用于监控端到端延迟。两者差值持续增大,往往意味着消费积压或网络抖动。四、误区与复盘
实践中常见的误区有以下几类:
- 把快照当增量用:误以为每条快照都是全量,导致下游重复计算或状态覆盖。应明确快照是状态切片,增量是变化量。
- 忽略序号连续性:逐笔与增量消息若不做 seqNum 校验,丢一条消息可能几天后才被发现,且难以定位。
- 用 JSON 承载高频行情:字段名重复传输,体积膨胀数倍,序列化开销高,直接拖低吞吐。
- 分区键选择不当:若用随机分区,同一 symbol 的逐笔消息会散落到不同分区,顺序无法保证。必须用 symbol 作为 key。
- 混淆交易所时间与本地时间:在延迟统计与回测中造成系统性偏差。
max.in.flight.requests.per.connection 设为 1 或配合幂等使用,否则重试可能导致消息乱序。复盘时可以用一张清单快速核对数据模型是否建模正确:
| 数据类别 | 关键字段 | Kafka 清理策略 | 顺序要求 |
|---|---|---|---|
| Level-1 快照 | symbol, lastPrice, bid1, ask1 | compact | 弱 |
| Level-2 快照 | symbol, bids[], asks[] | compact | 弱 |
| 逐笔委托 | orderId, side, price, qty, type | delete | 强 |
| 逐笔成交 | tradeSeq, price, qty, side | delete | 强 |
| 增量更新 | seqNum, prevSeqNum, changes[] | delete | 强 |
把这张表贴在设计文档里,每次新增行情源时逐项核对,可以避免绝大多数因数据模型理解偏差导致的线上事故。数据模型是行情管道的“地基”,地基不稳,后面的吞吐优化与容错设计都无从谈起。