Tick 一直在流入、还会乱序和迟到,程序什么时候才应该输出这一分钟的 K 线?

流处理:Flink 与 Spark Streaming

约 11 分钟6 道自测更新于 2026-10-28

一句话记住流处理用事件时间、窗口和水位线在准确性与延迟之间做取舍,再用状态与检查点恢复;Flink 偏原生流和复杂状态,Spark Structured Streaming 便于批流共用 API。

01学完你能做到

区分有界与无界数据、事件时间与处理时间;解释窗口、水位线、迟到数据、状态和检查点;比较 Flink 与 Spark Structured Streaming,并为实时行情选择处理方案。

02这课解决什么问题

Tick 一直在流入、还会乱序和迟到,程序什么时候才应该输出这一分钟的 K 线?流处理要同时处理“结果尽快出来”“数据尽可能完整”和“程序重启后不丢不重”这几个目标。

03流处理的核心概念

  • 有界与无界数据:昨天全天 Tick 有终点,属于有界数据;今天持续到来的行情没有终点,属于无界数据。
  • 事件时间与处理时间:事件时间是成交发生的时间,处理时间是程序收到或处理它的时间。行情聚合通常应以事件时间为准。
  • 窗口:把无限流切成可计算的小段。滚动窗口互不重叠,滑动窗口按间隔计算最近一段时间,会话窗口按活跃间隔切分。
  • 水位线:框架对事件时间推进程度的估计,表示早于某个时间的数据基本到齐,用来触发窗口输出。
  • 迟到数据:水位线之后才到的数据。可以丢弃、更新原结果,或进入旁路单独处理。
  • 状态:窗口中累积的开高低收、成交量等中间结果,需要持久化和恢复。
  • 检查点:定期保存算子状态和输入位置。故障后从检查点恢复并重读消息,可以在上下游配合时实现精确一次效果。

04Flink 与 Spark Structured Streaming

Flink 是原生流处理引擎,事件时间、状态管理和低延迟处理能力突出,适合实时指标、风控和复杂事件检测。Spark Structured Streaming 通常采用微批执行,与 Spark 批处理共用 DataFrame API,适合已有 Spark 技术栈的团队;具体延迟和投递保证取决于查询、源、汇和配置。

没有必要一开始就引入大型流处理集群。如果只是每分钟聚合、允许秒级延迟,Kafka 消费者攒批后写入数据库再聚合,可能更简单。选择框架要以实际乱序分布、延迟目标、状态大小和运维能力为依据。

05事件时间、水位线与 K 线

Kafka Tick -> 按证券分组 -> 事件时间一分钟窗口
                         -> 水位线越过窗口终点
                         -> 输出暂定/完成 K 线
                         -> 迟到数据进入修正或旁路

容忍乱序时间越短,输出越快但迟到数据越多;容忍时间越长,结果更完整但延迟和状态开销更大。水位线不是交易所保证“绝对到齐”的证明,只是框架根据策略做出的推进判断。

下面代码用纯 Python 模拟最小语义,不是真实 Flink 作业。

from datetime import datetime, timedelta

def parse(s):
    return datetime.fromisoformat("2026-09-15T" + s)

events = [
    ("09:30:05", 10.00, 500), ("09:30:40", 10.05, 300),
    ("09:31:02", 10.02, 200), ("09:31:30", 10.08, 100),
    ("09:30:58", 10.10, 400), ("09:32:10", 10.06, 600),
]

def run(max_out_of_order_seconds):
    state, closed = {}, set()
    max_event_time = datetime.min
    late = []
    for ts_text, price, volume in events:
        ts = parse(ts_text)
        window = ts.replace(second=0)
        if window in closed:
            late.append(ts_text)
            continue
        item = state.setdefault(window, {
            "open": price, "high": price, "low": price,
            "close": price, "volume": 0,
            "first": ts, "last": ts,
        })
        if ts < item["first"]:
            item["open"], item["first"] = price, ts
        if ts >= item["last"]:
            item["close"], item["last"] = price, ts
        item["high"] = max(item["high"], price)
        item["low"] = min(item["low"], price)
        item["volume"] += volume
        max_event_time = max(max_event_time, ts)
        watermark = max_event_time - timedelta(seconds=max_out_of_order_seconds)
        for start in sorted(set(state) - closed):
            if start + timedelta(minutes=1) <= watermark:
                closed.add(start)
    return state, late

收盘价必须按事件时间取窗口内最后一笔,而不是按到达顺序取最后一笔。午休、停牌或冷门证券长时间没有新事件时,水位线也可能不前进,需要空闲检测或交易日历驱动。

06常见误区

  1. 用处理时间聚合,网络抖动和重启会让同一数据产生不同结果。
  2. 凭经验拍一个乱序容忍时长,没有先统计数据分布。
  3. 忽略没有事件的窗口,导致窗口不关闭或错误补出成交。
  4. 看到“精确一次”就认为外部数据库和下游服务绝不会重复;外部系统仍需事务或幂等。
  5. 把盘中实时 K 线当最终结果;迟到数据和盘后全量重算可能造成修订。

07延伸阅读

08一分钟讲给别人

试着用自己的话讲一遍流处理就是边收数据边算结果。Tick 会乱序、迟到,框架用水位线判断某一分钟是否基本到齐,到齐后输出,晚到数据再修正或旁路。等得越久通常越完整但越慢;Flink 偏低延迟和复杂状态,Spark 便于批流共用一套代码。

想一想

6 道自测

先在心里答一遍,再展开对照。能说清理由,比答对数字更重要。

  1. 01事件时间和处理时间有什么区别?

  2. 02水位线的作用是什么?

  3. 03乱序容忍时间过大有什么代价?

  4. 04为什么实时 K 线可能需要盘后重算?

  5. 05Flink 和 Spark Structured Streaming 的主要取舍是什么?

  6. 06检查点解决什么问题?

已看 0/6 题

练动手练习

任务:把模拟程序的乱序容忍时长分别设为 5 秒、35 秒和 60 秒,比较 09:30 窗口的输出时间、成交量和迟到记录;写出一个暂定结果、修订结果和最终结果的状态字段设计。

完成这一课

完成这一课

读完讲义、看完自测答案,这一课就算学完了。

  • 读完讲义
  • 看完全部自测

本课为投资者教育内容,案例数字均为示意,不构成任何投资建议,也不承诺收益。规则和费率会变,实际操作前请以交易所、监管机构和产品的最新公告为准。