2. 实时计算基础理论:流处理与批处理的区别、事件时间与处理时间、水位线(Watermark)机制、背压(Backpressure)原理

各位同学,今天我们来聊聊实时计算引擎在量化交易中的那些基础理论。说实话,这些概念听起来有点抽象,但你在实际做策略的时候,天天都会跟它们打交道。我刚开始接触这块时,也踩过不少坑,今天就把我的经验分享给你们。

2.1 流处理与批处理的区别

先问大家一个问题:你平时做回测,用的是历史数据对吧?那叫批处理。但如果你要跑实盘,数据是一笔一笔进来的,那就是流处理。这两者有什么区别?

批处理,说白了就是「攒够了再算」。你把一段时间的数据收集起来,然后一次性计算。比如每天收盘后,把当天的所有成交数据拉下来,算个均值、方差什么的。这种方式的好处是简单、稳定,但缺点也很明显——延迟高。

流处理呢?它是「来一条算一条」。每来一笔行情数据,系统立刻处理。比如你监控着某只股票的逐笔成交,一旦发现大单买入,马上触发你的交易逻辑。延迟可以做到毫秒级。

我个人的习惯是:回测用批处理,实盘用流处理。但要注意,这两者的逻辑有时候会不一样。举个例子:

// 批处理:计算过去5分钟的均价
double avgPrice = dataWindow.stream()
    .filter(tick -> tick.time >= now - 5 * 60 * 1000)
    .mapToDouble(Tick::getPrice)
    .average()
    .orElse(0.0);

// 流处理:每来一笔数据,更新滑动窗口的均价
public void onTick(Tick tick) {
    window.add(tick);
    window.removeExpired(now - 5 * 60 * 1000);
    double avgPrice = window.getAverage();
}

你看,批处理是「事后诸葛亮」,流处理是「实时反应」。在量化交易中,我们大部分场景都需要流处理,因为市场不等人。

核心区别总结:

  • 延迟:批处理分钟级到小时级,流处理毫秒级到秒级
  • 数据范围:批处理处理全量数据,流处理处理增量数据
  • 计算模式:批处理一次性计算,流处理持续计算
  • 状态管理:批处理无状态,流处理需要维护状态

2.2 事件时间与处理时间

嗯,这里有个容易混淆的概念——事件时间和处理时间。我当年刚入行时,就因为这个吃过亏。

事件时间,就是数据实际发生的时间。比如一笔成交在 14:30:15 发生,那事件时间就是 14:30:15。这个时间是由数据源(比如交易所)打上的,不会变。

处理时间,是数据到达你的系统的时间。比如因为网络延迟,这笔成交数据在 14:30:18 才到你的服务器,那处理时间就是 14:30:18。

你想想看,如果网络一直稳定,这两个时间差不多。但现实是网络会抖动,数据会乱序。比如:

  • 14:30:15 的成交,14:30:18 到达
  • 14:30:16 的成交,14:30:17 到达(反而先到了)

如果你用处理时间来计算,那 14:30:16 的成交会被先处理,14:30:15 的反而后处理。这会导致你的指标计算完全乱掉。

我的建议:在量化交易中,尽量使用事件时间。因为交易策略的逻辑是基于市场实际发生的时间顺序,而不是数据到达的顺序。我曾经有一个策略,因为用了处理时间,导致信号延迟了 3 秒,回测看起来很好,实盘一跑就亏钱。

2.3 水位线(Watermark)机制

说到事件时间,就不得不提水位线。这是个什么东西呢?

简单来说,水位线是一个「时间标记」,告诉系统:到这个时间点为止,数据应该都到齐了。如果还有更早的数据,那就是迟到的数据,你可以选择丢弃或者特殊处理。

举个例子:你设置水位线为 5 秒。也就是说,系统认为 5 秒前的数据应该都到了。比如现在是 14:30:20,那系统认为 14:30:15 之前的数据都到齐了。如果这时候突然来了一笔 14:30:14 的数据,那就是迟到数据。

为什么会这样?因为网络延迟、系统负载等原因,数据可能乱序到达。水位线就是用来处理这种乱序的。

// 伪代码:水位线机制
Watermark watermark = new Watermark(5, TimeUnit.SECONDS);

public void onTick(Tick tick) {
    long eventTime = tick.getEventTime();
    long currentWatermark = watermark.getCurrentWatermark();
    
    if (eventTime < currentWatermark) {
        // 迟到数据,特殊处理
        handleLateData(tick);
    } else {
        // 正常处理
        processTick(tick);
        // 更新水位线
        watermark.update(eventTime);
    }
}

注意:水位线设置太短,会导致大量迟到数据被丢弃,影响策略准确性。设置太长,又会增加延迟。我一般建议根据网络延迟的 99 分位值来设置,比如网络延迟 99% 的情况下不超过 2 秒,那水位线就设 2 秒。

2.4 背压(Backpressure)原理

最后聊聊背压。这个词听起来很专业,其实道理很简单——就是「上游太快,下游跟不上」。

在量化交易中,行情数据是源源不断的。比如某只股票在开盘时,每秒可能有几千笔成交。如果你的处理逻辑比较复杂,比如要计算多个指标、更新多个窗口,那处理速度可能跟不上数据到达的速度。

这时候会发生什么?数据会堆积在缓冲区里,内存占用越来越高,最终导致 OOM(内存溢出)或者系统崩溃。

背压机制就是用来解决这个问题的。它让下游告诉上游:「我吃不下了,你慢点发」。具体实现方式有几种:

  • 阻塞式:下游处理不过来时,阻塞上游的数据发送。简单粗暴,但可能导致整个系统停顿。
  • 丢弃式:下游处理不过来时,丢弃部分数据。适合对数据完整性要求不高的场景。
  • 反压式:下游通过某种协议(比如 TCP 的滑动窗口)告诉上游降低发送速率。这是最优雅的方式。

我个人在项目中遇到过这种情况:有一次做高频策略,行情数据量突然暴增,系统直接挂了。后来加了背压机制,虽然会丢弃一些数据,但至少系统不会崩溃。嗯,这里要注意,丢弃数据对量化交易来说是有风险的,所以最好还是做好容量规划。

背压处理的最佳实践:

  1. 先做容量评估,确保系统能处理峰值流量的 2-3 倍
  2. 如果还是处理不过来,优先丢弃非关键数据(比如 tick 级别的数据可以降级为 1 秒快照)
  3. 监控背压状态,一旦触发背压,立刻报警

知识体系总览

下面这张图,是我画的一个知识体系总览,帮你理清这些概念之间的关系:

实时计算引擎核心概念 流处理 vs 批处理 • 流处理:来一条算一条,低延迟 • 批处理:攒够了再算,高延迟 • 量化场景:实盘用流,回测用批 事件时间 vs 处理时间 • 事件时间:数据实际发生的时间 • 处理时间:数据到达系统的时间 • 建议:量化策略用事件时间 水位线(Watermark) • 时间标记:数据应该到齐的时间点 • 处理乱序:识别迟到数据 • 设置建议:基于网络延迟 99 分位 背压(Backpressure) • 问题:上游太快,下游跟不上 • 方案:阻塞/丢弃/反压 • 最佳实践:容量规划 + 降级 核心目标:低延迟、高吞吐、数据一致性 流处理 + 事件时间 + 水位线 + 背压 = 可靠的实时计算引擎

好了,这一章的内容就到这里。这些基础理论,是后面所有实战内容的地基。你把这些概念吃透了,后面学 Flink、Spark Streaming 这些框架时,就会轻松很多。


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