1. 课程导入与场景锚定
在实时数据系统中,我们面对的往往不是"一批已经落库的历史数据",而是一条持续到达、永不停歇的事件流。逐笔特征增量更新与低延迟计算,正是解决"如何在事件到达的瞬间,用尽可能小的计算代价,把特征值维护到最新状态"这一核心问题的技术体系。它要回答的不是"最终结果是多少",而是"在每一个新事件到达后的极短时间内,结果应该是多少,以及如何保证这个结果既快又对"。
核心问题:从"全量重算"到"增量维护"
假设我们要维护一个交易账户的"近1分钟成交金额"特征。最朴素的做法是:每来一笔成交,就把过去60秒内的所有成交重新加总一遍。当每秒只有几笔成交时,这没问题;但当每秒有数万笔成交、且需要同时维护上千个特征时,全量重算的代价会迅速失控。增量更新的思路是:新事件到达时,只做"加上新值、减去过期值"的局部修正,把每次更新的复杂度从与窗口长度相关,降到与单次事件相关。
这里有一个必须提前明确的参照约定:本文所有"窗口"均指事件时间窗口,即以事件自带的时间戳为准,而非以服务器处理时刻为准。窗口右边界随最新事件时间推进,左边界为右边界减去窗口长度。过期判定使用"事件时间戳 < 右边界 − 窗口长度"这一条件。这一约定在后续所有章节和图示中保持一致。
增量更新的难点并不在"加"和"减"本身,而在于三件事:其一,状态如何组织——需要能快速找到"哪些值过期了";其二,乱序与重复如何处理——事件时间可能回退,同一笔成交可能重复投递;其三,延迟如何控制——更新链路中任何一次阻塞都会传导为端到端延迟。
典型业务场景
高频交易场景中,策略需要在微秒到毫秒级拿到"最近N笔成交的买卖压力差""盘口失衡度"等特征。这类特征窗口短、更新频繁,对延迟极其敏感,通常要求单次特征更新在百微秒量级完成,且不允许因垃圾回收或锁竞争产生长尾抖动。
实时风控场景中,特征窗口更长(如近10分钟交易频次、近1小时设备聚集度),但更强调正确性与可追溯性。乱序事件、补发数据、重复消息都必须被正确处理,否则风控判断会出现"漏判"或"误判"。这类场景往往接受毫秒到秒级延迟,但不能接受状态不一致。
行情监控场景介于两者之间,关注"近1分钟成交量突增""买卖价差异常扩大"等指标,需要在高吞吐下保持稳定输出,并能在指标越界时及时触发告警。
三类场景的共同点是:特征都是"随时间滑动的聚合量",都需要在事件流上持续维护,都对"更新代价"和"结果新鲜度"有明确要求。差异在于延迟预算、正确性等级和乱序容忍度的取舍。
学习路径与实战环境准备
本课程的学习路径遵循"先建立状态模型,再优化更新路径,最后压测延迟"的顺序。第一阶段理解窗口状态的组织方式与增量修正的数学基础;第二阶段处理乱序、重复、迟到数据带来的正确性问题;第三阶段进入工程实现,包括数据结构选型、内存布局、无锁化与批处理;第四阶段做延迟测量与长尾治理。每一阶段都会给出可运行的示例与可观察的指标。
实战环境建议准备如下:一台具备多核 CPU 的开发机,安装 JDK 17 或以上(或 Python 3.10+,视示例语言而定),准备一个可回放的事件数据集。为便于复现,我们约定一个教学示例数据集格式:每行一条事件,字段为 event_id, event_time_ms, symbol, side, price, qty,其中 event_time_ms 为毫秒级事件时间戳。示例数据可由脚本生成,属于合理假设,不代表任何真实市场数据。
event_id,event_time_ms,symbol,side,price,qty
E0001,10001120,AAA,BUY,10.25,300
E0002,10001135,AAA,SELL,10.26,200
E0003,10001150,AAA,BUY,10.24,500
E0004,10001162,AAA,SELL,10.27,100
E0005,10001128,AAA,BUY,10.25,400 # 乱序到达
以"近1分钟成交金额"为例,增量维护的核心状态可以是一个按事件时间有序的队列,外加一个累计金额变量。新事件到达时,先按 event_id 查重,若已存在则丢弃;否则将 price × qty 累加到总额,并把事件插入有序位置;随后从队首弹出所有 event_time_ms < 最新事件时间 − 60000 的事件,逐条从总额中扣除。这样每次更新只涉及常数次加减和一次插入/弹出,代价与窗口内事件数量无关。
判断实现是否达标的观察点有三个:一是正确性,用全量重算的结果做对照,逐事件比对增量结果,误差应为零(浮点场景需约定容差);二是延迟,记录每个事件从进入到特征输出的耗时,观察 P50、P99、P999 分位数,而非只看平均值;三是稳定性,在持续高吞吐下观察延迟是否出现周期性尖峰,这通常指向内存分配或锁竞争问题。