3. 消息唯一性判定方法

去重的第一步不是“删”,而是“判”——判断两条消息是不是同一条。行情流里常见的重复来源有三类:交易所重推(快照补发、增量重发)、网关重连后的补拉、以及下游多路订阅造成的同一消息被多次投递。如果唯一性判定规则设计不当,轻则漏去重导致重复成交/重复计数,重则误去重把两条真实不同的行情当成一条丢掉,造成盘口缺口。因此本节聚焦三种主流判定方法:基于消息ID、基于业务键、基于哈希指纹,并给出选型原则。

一、基于消息ID:最直接,但依赖上游可靠性

消息ID是发送方为每条消息分配的唯一标识,可能是交易所的序列号(sequence number)、行情网关自增的消息序号,或消息中间件生成的全局ID。判定逻辑非常简单:维护一个已见ID集合(或滑动窗口内的ID位图),新消息到达时查集合,命中即判重。

它的优势是判定成本低、语义明确、几乎不会误判——只要ID真的唯一。问题在于ID的“唯一性边界”由上游决定:交易所的序列号通常按频道(channel)或按交易对独立递增,跨频道不可比;网关重启后自增ID可能归零;不同上游汇聚到同一消费者时,ID空间可能冲突。因此使用消息ID去重,必须明确三件事:ID的作用域(哪个频道/哪个连接)、ID的单调性(是否严格递增、是否允许跳号)、以及重连后的ID重置策略。工程上常配合“序列号连续性校验”一起使用:既用ID判重,也用ID检测丢包。

二、基于业务键:跨源可比的语义唯一性

业务键是用消息中承载的业务字段组合出的唯一标识,行情场景最典型的是 Symbol + Exchange + Timestamp,必要时再加 Channel(如 depth/trade/ticker)和 Sequence。它不依赖上游分配的ID,因此天然适合多源汇聚、跨网关比对、以及历史回放与实时流对齐的场景。

业务键的难点在于“键的粒度”和“时间戳精度”。以逐笔成交为例,同一交易对在同一毫秒内可能有多笔成交,若时间戳只到毫秒,Symbol+Exchange+Timestamp 会把多笔真实成交误判为重复。此时需要把成交编号(trade id)或序号纳入键,或把时间戳精度提升到微秒/纳秒。另一个坑是时间戳语义不统一:交易所时间、网关接收时间、本地落库时间三者不同,混用会导致同一消息在不同环节生成不同键。实践中的做法是固定使用“交易所事件时间”作为键的一部分,并在接入层统一时区与精度。

三、基于哈希指纹:把整条消息压成一个短标识

哈希指纹是对消息的规范化内容(去掉传输层字段如接收时间、路由信息后)计算摘要,如对规范化JSON做 SHA-256 或 xxHash,取前若干字节作为指纹。它不关心哪个字段是“键”,只要内容一致指纹就一致,因此对字段顺序、可选字段缺失等结构差异不敏感——前提是先做规范化(字段排序、数值格式统一、空值处理)。

哈希指纹的优点是通用性强、误判率可通过位宽控制,适合消息结构复杂、没有稳定业务键、或需要“整包比对”的场景。代价是计算开销高于前两者,且对规范化极其敏感:同一业务含义但字段顺序不同、浮点表示不同(1.10 vs 1.1)、时间格式不同,都会得到不同指纹,导致去重失效。因此哈希指纹必须配套一套稳定的规范化规则,并随消息 schema 变更同步维护。

三种唯一性判定方法:输入 → 判定依据 → 风险点 消息ID 业务键 哈希指纹 上游分配的 seq / msgId 作用域:频道 / 连接 Symbol + Exchange + Timestamp 可加 Channel / TradeId 规范化内容 → 摘要 SHA-256 / xxHash 截断 已见ID集合 / 位图查重 命中即重复,O(1) 键集合查重 跨源可比,需定键粒度 指纹集合查重 整包比对,依赖规范化 风险 重连后ID归零 / 跨频道冲突 需配合序列号连续性校验 风险 同毫秒多笔被误判重复 时间戳语义需统一 风险 字段顺序/浮点格式差异 导致同义不同指纹 选型主线:单源且ID可信 → 消息ID;多源汇聚/回放对齐 → 业务键;结构复杂无稳定键 → 哈希指纹(可组合使用)

四、三种方法的对比与选型原则

把三者放在一起看,差异集中在四个维度:唯一性来源(上游分配 vs 业务语义 vs 内容摘要)、跨源可比性(ID弱、业务键强、指纹中)、计算与存储成本(ID最低、业务键中、指纹最高)、误判风险类型(ID怕作用域冲突、业务键怕粒度不足、指纹怕规范化不一致)。

维度消息ID业务键哈希指纹
唯一性来源上游分配业务字段组合规范化内容摘要
跨源/跨网关可比弱(受作用域限制)强中(需同一规范化规则)
单条判定开销极低低中高(需哈希计算)
典型误判重连归零、跨频道冲突键粒度过粗格式差异致同义不同指纹
适用场景单交易所单连接、强序列语义多源汇聚、回放对齐、跨系统比对结构复杂、无稳定键、整包比对

选型可以遵循一条主线:先看数据是否单源、ID是否可信。若消息来自单一交易所的单一连接,且序列号严格递增、重连有明确的续传协议,直接用消息ID最省成本,并顺带做连续性校验。若消息来自多个网关或多个上游、需要与历史回放对齐,业务键更合适,因为它不依赖某一路的ID空间。若消息结构复杂、字段多且没有天然的业务主键,或需要判断“整包是否完全一致”,则用哈希指纹,但必须先把规范化规则固化下来。

实际系统里三者往往组合使用,形成分层判定:第一层用消息ID做快速拦截,成本最低;第二层对通过的消息用业务键做语义级判重,覆盖跨源重复;第三层在需要严格整包比对的场景(如快照与增量合并前的校验)再计算哈希指纹。分层的好处是让高成本判定只作用于少量候选,而不是对全量消息都算哈希。

五、一个可执行的判定示例

假设接入层收到一条深度增量消息,字段包含 exchange、symbol、channel、ts(交易所事件时间,微秒)、seq、bids、asks。可以按如下顺序判定:

// 第一层:消息ID(作用域 = exchange + channel + symbol)
idKey = exchange + "|" + channel + "|" + symbol + "|" + seq
if (seenIds.contains(idKey)) return DUPLICATE

// 第二层:业务键(时间戳用交易所事件时间,微秒精度)
bizKey = exchange + "|" + symbol + "|" + channel + "|" + ts
if (seenBizKeys.contains(bizKey)) return DUPLICATE

// 第三层:哈希指纹(仅对前两层未命中的候选计算)
canonical = normalizeJson({exchange, symbol, channel, ts, seq, bids, asks})
fp = xxhash64(canonical)
if (seenFingerprints.contains(fp)) return DUPLICATE

return NEW

这里的 normalizeJson 需要明确规则:对象键按字典序排列、数值统一为定点字符串(避免 1.10 与 1.1 差异)、时间统一为微秒整数、数组保持原序。判定结果要能被观察:为每层命中计数并打点,若第一层命中率异常升高,往往意味着上游在重推;若第二层命中而第一层未命中,说明存在跨源重复;若第三层频繁命中前两层未命中的消息,则要检查规范化规则是否把不同消息压成了同一指纹。这些计数就是判断去重策略是否健康的最直接依据。

去重集合需要设置合理的过期窗口。行情消息的重复通常发生在秒级到分钟级,窗口过长会占用内存,过短则漏去重。可按“最大重推延迟 × 安全系数”设定,例如重推延迟上限 30 秒时,窗口取 5 分钟。
不要用“消息内容看起来一样”代替严格判定。浮点精度、字段顺序、可选字段缺失都会让肉眼相同的两条消息在程序里不同;反之,键粒度过粗又会让真实不同的消息被判为重复。判定规则必须显式定义并写入测试用例。