3. 实时计算引擎:Flink核心概念与Spark Streaming对比

说到实时计算引擎,Flink 和 Spark Streaming 是绕不开的两个名字。我做了这么多年用户行为流,这两个引擎都深度用过。今天咱们就聊聊 Flink 的核心概念,再跟 Spark Streaming 做个对比。

先抛个问题:为什么现在大家都在聊 Flink?说白了,因为用户行为场景对实时性要求越来越高。你想想看,用户点击一个按钮,你希望多久能看到这个事件?几秒?还是几分钟?Flink 的设计哲学就是「事件一旦发生,立刻处理」。

3.1 DataStream:一切皆流

Flink 里最核心的抽象就是 DataStream。你可以把它理解成一个永不停止的数据管道。用户点击、浏览、下单,这些事件源源不断地流进来,Flink 就在这个管道上做各种转换操作。

核心要点:DataStream 是 Flink 对无限数据流的统一抽象。无论数据来自 Kafka、Socket 还是文件,最终都表示为 DataStream。

我刚开始用 Flink 时,总觉得 DataStream 跟 Spark 的 DStream 差不多。后来踩了个坑才发现——完全不是一回事。Flink 的 DataStream 是 逐条处理 的,而 Spark Streaming 是 微批次 的。这个区别,后面细说。

看个简单的例子:

// 从 Kafka 读取用户行为事件
DataStream<UserAction> actions = env
    .addSource(new FlinkKafkaConsumer<>("user_actions", 
        new JSONDeserializationSchema(), 
        properties));

// 过滤出点击事件
DataStream<UserAction> clicks = actions
    .filter(action -> "click".equals(action.getType()));

// 按用户分组,统计每个用户的点击次数
DataStream<Tuple2<String, Long>> clickCounts = clicks
    .keyBy(UserAction::getUserId)
    .map(new CountFunction());

这段代码很直观吧?从 Kafka 读数据,过滤,分组,统计。Flink 的 API 设计就是让你像写普通 Java 程序一样写流处理逻辑。

3.2 时间语义:事件时间 vs 处理时间

这是 Flink 里最容易搞混的概念,也是我面试时最喜欢问的问题。为什么?因为实际项目中,时间语义选错了,结果就全错了。

先看定义:

时间语义 含义 典型场景
事件时间 (Event Time) 事件实际发生的时间,通常由客户端生成 用户行为分析、日志处理
处理时间 (Processing Time) 事件到达 Flink 算子的时间 实时监控、告警
摄入时间 (Ingestion Time) 事件进入 Flink 系统的时间 介于两者之间,较少使用

举个例子你就明白了。用户 A 在 10:00:00 点击了按钮,但这个事件因为网络延迟,10:00:05 才到 Flink。那么:

  • 事件时间 = 10:00:00(用户实际点击的时间)
  • 处理时间 = 10:00:05(Flink 收到的时间)

做用户行为分析,你肯定希望按事件时间算。不然的话,用户 10 点点击,你统计到 10:01 的窗口里,这不就乱套了吗?

我曾经踩过的坑:有个项目统计用户日活,默认用了处理时间。结果凌晨流量高峰时,Kafka 消费延迟了 30 秒,导致大量事件被划到下一个窗口。日活数据直接飘了 15%。从那以后,用户行为相关的场景我全部强制使用事件时间。

Flink 里启用事件时间很简单:

// 设置时间语义为事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

// 在 source 中提取事件时间戳
DataStream<UserAction> actionsWithTimestamp = actions
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<UserAction>forBoundedOutOfOrderness(Duration.ofSeconds(5))
            .withTimestampAssigner((event, timestamp) -> event.getTimestamp())
    );

注意那个 forBoundedOutOfOrderness(Duration.ofSeconds(5))。这是 Flink 处理乱序事件的利器。什么意思呢?就是允许事件最多迟到 5 秒。超过 5 秒的,就当作迟到数据丢弃或侧输出处理。

3.3 窗口:把无限流切成有限块

流是无限的,但计算需要边界。窗口就是 Flink 切分无限流的工具。用户行为分析里,窗口无处不在——统计每分钟的 PV、每小时的 UV、每天的留存率。

Flink 支持几种窗口类型:

窗口类型 说明 用户行为场景
滚动窗口 (Tumbling Window) 固定大小,不重叠 每分钟 PV 统计
滑动窗口 (Sliding Window) 固定大小,可重叠 最近 5 分钟的活跃用户数
会话窗口 (Session Window) 按不活动间隔划分 用户一次访问会话
全局窗口 (Global Window) 所有数据一个窗口 自定义触发器场景

看个滚动窗口的例子:

// 每分钟统计每个用户的点击次数
DataStream<Tuple2<String, Long>> pvPerMinute = clicks
    .keyBy(UserAction::getUserId)
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .aggregate(new CountAggregate(), new WindowResultFunction());

这里有个细节:keyBy 之后才能开窗口。为什么?因为窗口计算需要按 key 分组,每个 key 有自己的窗口状态。你想想看,统计每个用户的 PV,当然要按用户分组。

个人经验:会话窗口在用户行为分析里特别有用。比如判断用户是否离开,通常设置 30 分钟不活动就结束会话。Flink 的会话窗口天然支持这个逻辑,不用自己维护状态。

3.4 Flink vs Spark Streaming:核心差异

好,现在来对比一下这两个引擎。我两个都用过,说说真实感受。

对比维度 Flink Spark Streaming
处理模型 真正的逐条流处理 微批次处理(Mini-batch)
延迟 毫秒级 秒级(通常 1-5 秒)
事件时间支持 原生支持,非常完善 支持,但精度受批次影响
状态管理 内置状态后端,支持大状态 依赖外部存储或 RDD
Exactly-Once 语义 端到端支持 需要配置,有一定开销
SQL 支持 Flink SQL,动态表 Structured Streaming

说说我的实际感受。Spark Streaming 的微批次模型有个天然问题:批次间隔决定了延迟下限。你设置 2 秒一批,那延迟至少 2 秒。Flink 是逐条处理,事件来了就处理,延迟可以做到几十毫秒。

但 Spark Streaming 也有优势。如果你已经在用 Spark 做批处理,那 Spark Streaming 可以复用很多基础设施。而且 Spark 的生态更成熟,文档更全。

怎么选?我个人的建议是:

  • 用户行为实时分析 → 选 Flink。事件时间、乱序处理、低延迟,这些是用户行为场景的刚需。
  • 准实时 ETL → 两个都可以。如果团队 Spark 经验丰富,用 Spark Streaming 更稳妥。
  • 需要复杂事件处理 (CEP) → 选 Flink。Flink 的 CEP 库非常强大,Spark 没有原生支持。

一句话总结:Flink 是为流而生,Spark Streaming 是用批模拟流。选哪个,取决于你的场景和团队。

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到 Flink 的 DataStream、时间语义、窗口是怎么配合的,以及跟 Spark Streaming 的对比关系。

Flink 核心概念与 Spark Streaming 对比 数据源 (Kafka/Socket) DataStream (核心抽象) 时间语义 窗口操作 事件时间 处理时间 滚动窗口 滑动窗口 会话窗口 Flink:逐条流处理,毫秒延迟 Spark Streaming:微批次,秒级延迟 事件时间原生支持 · 状态管理完善 生态成熟 · 批流统一 · 学习成本低

这张图把 Flink 的核心脉络理清楚了。从数据源进来,经过 DataStream 抽象,然后选择时间语义和窗口类型,最后输出结果。右边是跟 Spark Streaming 的对比,帮你快速定位差异。

嗯,这一章的内容就到这。Flink 的概念确实比 Spark Streaming 多,但每个概念都有它的设计意图。你只要理解了「流是无限的,计算需要边界」这个核心思想,后面学起来就顺了。