唯品会 2018 校招的实时开发笔试题,放在今天复盘依然很有意思。那年头正是各大电商平台狂补实时数据能力的阶段,大促实时大屏、实时风控、实时个性化推荐,全部压在实时计算引擎上。而唯品会作为特卖电商,对实时性的要求比普通平台更苛刻——一场闪购活动,流量在开售瞬间涌进来,数据链路能不能撑住,直接决定了运营能不能看到实时销售曲线。我当时看到这套笔试题的第一反应是:这不是单纯的“Flink API 默写考试”,它更像是在筛选“有没有真正理解实时链路”的人。这篇文章我打算抛开具体题型,把题背后想考察的实时开发核心逻辑拆开揉碎,讲清楚电商实时开发的底层问题和应对思路,相信对准备数据开发岗位、或者正在做实时数仓的同行都有参考价值。我会结合当时的题目复盘、技术选型背景,以及后来在项目里踩过的真实坑,尽量把这事讲透。
1. 一场电商笔试背后的“实时战场”到底长什么样
1.1 电商公司为什么单独招“实时开发”
在 2018 年,实时开发已经从“锦上添花”变成了电商的刚需。你可能觉得,离线数仓跑个 T+1 报表不就够了吗?但电商里有很多场景是离线完全覆盖不了的:
- 大促实时大屏:运营盯着屏幕看 GMV、订单量、退款率,如果数据延迟十分钟,运营根本没法做实时调控。
- 实时风控:薅羊毛、盗号、恶意下单,这些行为以秒级为单位发生,等离线任务跑出来,钱早就被薅走了。
- 实时推荐与个性化:用户浏览了一个商品,系统需要在几百毫秒内更新推荐桶,从“看过”变成“猜你喜欢”。
这些场景决定了实时开发不是“会写 SQL 就行”,而是要对流式计算的整个链路有系统认知。唯品会作为特卖平台,一次品牌闪购可能只有几个小时,实时指标直接决定运营是否能及时补货、调整投放策略,所以他们对实时开发的要求更高。
1.2 笔试题的关键脉络:从接口到引擎的选择题
回看当时的技术栈,很多人会以为唯品会考的是 Flink,但实际上那年正是流计算引擎百花齐放、交替迭代的阶段。Storm 还在大量存量业务上运行,Spark Streaming 因为生态优势占据了不少份额,Flink 开始成为新宠。笔试里有相当一部分题,表面是在考 API 和代码,实际上是在考察你对“实时计算引擎选型”的理解。
比如典型的题目会问:Kafka 中的消息如何保证有序?如何实现精确一次性消费?请比较 Storm、Spark Streaming 和 Flink 在实时性上的差异。这些题目不是考察你有没有背过源码,而是考察你有没有在真实的流处理场景里处理过这些问题。你只有真正用引擎处理过乱序、积压、重复消费,才能在考场上有条理地答出来。
2. 老牌流处理三件套对比:Storm、Spark Streaming 和 Flink 的同台较量
2.1 为什么 Storm 先火起来又率先被边缘化
Storm 是流计算领域的老前辈,它的核心模型是拓扑(Topology),数据以 Tuple 形式在 Spout 和 Bolt 之间流动。它的优势是延迟极低,毫秒级,而且编程模型直观——把数据处理流程画成 DAG,照图写代码就行了。
但 Storm 的问题也很明显:它本身的“流”是逐条处理的,却不自带状态管理。你要做窗口计数、去重之类的操作,需要自己去外部存储(Redis、HBase)里维护状态,这就引入了大量额外的网络开销和一致性难题。Storm 的 at-least-once 语义虽然可以通过手动 ACK 做到不丢数据,但重复数据要靠业务方自己去幂等处理。2018 年的时候,很多做 Storm 的团队已经累得不行,每天都在跟数据重复、状态不一致做斗争。
笔试里如果考 Storm,大概率会从“可靠性机制”下手,比如问 Spout 的 emit 和 ack/fail 流程。这种题想答好,不能只背 API,要理解它的 ack 机制本质上是“追踪消息树”,一旦某个 Bolt 处理失败,整棵消息树会重放,这就意味着下游必须幂等。
2.2 Spark Streaming 的“微批”思路解决了什么,又牺牲了什么
Spark Streaming 的核心思路是“微批”:把实时流切成一个个小批次,比如每 2 秒一个 batch,然后用 Spark 的批处理引擎去算。这种方式的最大好处是能直接复用 Spark 生态,Spark SQL、MLlib 都能衔接上来,而且基于 RDD 的容错机制相对成熟,exactly-once 实现起来比 Storm 容易。
但“微批”带来的天然缺陷就是:延迟至少是一个批次的间隔。即便你把 batch 间隔调到 500ms,它依然是“准实时”而非“真实时”。另外,Spark Streaming 的窗口计算是基于 batch 拼接出来的,窗口对齐、数据乱序处理都不够精细。比如一个滑动窗口要每 5 秒滑一次,如果 batch 间隔是 2 秒,会出现窗口边界和 batch 边界对不齐的问题,体验非常别扭。
那时候笔试如果问“Spark Streaming 为什么能保证 exactly-once,却依然不适合某些场景”,答案的核心就是“延迟”和“窗口模型”。面试官其实在等你说出“微批不等于流”这句话。
2.3 从 2018 年的视角看 Flink:为什么它的窗口模型更适合电商
Flink 在 2018 年势头已经很猛了。它的核心是真正的流式处理引擎,数据一来就处理,延迟可以做到毫秒级,而且天然支持事件时间(Event Time)和处理时间(Processing Time)两种时间语义。更有价值的是 Flink 的窗口模型非常灵活:滚动窗口、滑动窗口、会话窗口,全都内置,窗口生命周期、触发条件、迟到数据处理都有精细的控制。
电商场景最喜欢 Flink 的几点包括:内置状态管理(Keyed State)、精确一次语义(端到端配合 checkpoint 和 Kafka 可以做到)、以及完善的背压机制。以前用 Storm 要自己维护状态,用 Spark Streaming 要容忍延迟,Flink 基本把这两大痛点同时解决了。2018年的笔试题里如果出现“给定一个场景:大促期间统计每 5 分钟各品类销售额,你会选择哪种引擎”,最优答案就是 Flink,并且要说出“为什么不用 Storm 和 Spark Streaming”。
我当时复习时自己做了一张对比表,直到现在都还能背出来:
| 维度 | Storm | Spark Streaming | Flink |
|---|---|---|---|
| 处理模型 | 逐条处理 | 微批处理 | 逐条处理 |
| 延迟 | 毫秒级 | 秒级(取决于批次间隔) | 毫秒级 |
| 状态管理 | 弱,依赖外部存储 | 基于 RDD,需要自己维护 | 内置 Keyed State,功能完善 |
| 窗口支持 | 弱,需自己实现 | 支持但不够精细 | 滚动、滑动、会话窗口全支持 |
| 端到端精确一次 | 难实现 | 相对容易 | 配合 checkpoint 可实现 |
| 背压机制 | 需手动支持 | 有反馈机制但不原生 | 原生集成,自动反压 |
这张表放在笔试简答题里就是一套完整的“选型逻辑”。
3. 从真题反推底层原理:窗口、水位线与乱序处理
3.1 产品经理要的“实时”和工程师的“迟到数据”之间隔着多少秒
电商实时报表里最常听到的一句话是:“这个数据怎么还没出来?”但做实时开发的人都知道,实时不等于“即时完整”,因为网络延迟、业务重试、消息乱序,都会导致数据迟到的现象。
什么是乱序?用户在 10:00:00 产生了一条下单事件,但由于客户端网络抖动,这条消息在 10:00:03 才发到 Kafka;而用户在 10:00:01 产生的另一条事件,却在 10:00:02 就到了。你如果按照处理时间来算窗口,就会把 10:00:00 的事件算到 10:00:03 的那个窗口里,数据就不对了。所以必须在数据里带上“事件发生时间”(Event Time),而不是依赖处理时间(Processing Time)。
3.2 watermark 的设定不是拍脑袋,而是业务容忍度的量化
Flink 处理乱序的核心工具是水位线(Watermark)。水位线的含义是:时间戳小于等于水位线的事件,我认为都已经到齐了;在这之后的迟到数据,就要走迟到处理逻辑。
这里很多人犯的错误是:Watermark 到底设置多少秒?答案是看业务容忍度。如果你统计的是“当前在线人数”,那延迟 5 秒可能用户都走光了,Watermark 要设置得很小;但如果你统计的是“今日累计销售额”,迟到几分钟的数据也能接受,Watermark 可以给得宽松一些。
当时有一道真题大概是:统计最近 5 分钟热门商品排行榜,允许 30 秒以内的数据乱序,应该怎么设计 Window 和 Watermark。正确思路是:
- 使用 Event Time 作为时间字段
- 设置 Watermark = 当前最大事件时间 - 30 秒
- 使用滑动窗口,滑动的步长根据榜单刷新频率来定,比如每 1 分钟滑动一次
- 对于 Watermark 之后才到的数据,发到侧输出流(Side Output),等汇总时再决定要不要合并
3.3 一个典型的电商实时统计题:UV / PV 如何做对
笔试中高频出现的一类题是:统计某一小时内某商品的 PV / UV。PV 简单,来一条加一条即可;UV 就麻烦了,需要做去重。
实时 UV 的常见解法几种:
- 用 Redis 的 Set 数据结构,对每个商品存一个 user_id 集合,直接 SCARD 获取数量。优点是精确,缺点是内存开销大、大促时压力大。
- 用 HyperLogLog:内存极小,但误差约 0.81%,适合大促看板这种“看趋势”的指标,不适合精确对账。
- 用 Flink 的 KeyedState 在内存里去重:适合中小规模,但如果 key 比较大或数量很多,需要时刻注意状态大小。
我当时的答题思路是先给出方案对比,再说明取舍依据。面试官想看到的不是“死记硬背一个方案”,而是你面对不同量级、不同精确度要求时能权衡选型的能力。
4. 精确一次与幂等:实时链路里最难的一步棋
4.1 checkpoint 机制:到底把状态存在哪里
Flink 的 checkpoint 机制是它实现容错和精确一次的基础。简单说,它定期对每个算子状态做一次快照,当任务失败时,从最近一次成功的 checkpoint 恢复。这里有一个核心机制叫 barrier 对齐:Flink 会往数据流里插入特殊标记,所有上游算子看到这个标记后,把当前的状态保存下来,并对齐数据流向,保证所有算子保存的是同一个数据版本的状态。
真实场景中,checkpoint 的周期怎么设置很有讲究。设得太短,状态频繁持久化,性能和存储压力大;设得太长,故障恢复时数据丢失的时间窗口变大。我当时的经验是:默认 1 分钟起步,如果业务对数据完整性要求极高且状态不大,可以调到 30 秒;但如果是超大状态,千万不要把 checkpoint 周期调到 10 秒以下,否则会导致整个作业性能严重下降。
笔试里如果问“Flink 为什么能精确一次,Spark Streaming 为什么也能,两者有什么区别”,关键在于 Flink 的二阶段提交协议(Two-Phase Commit)。Flink 的 sink 在 checkpoint 时会做预提交,等所有事务都确认成功后统一提交,以此保证写入外部系统的数据和状态是一致的。
4.2 从 Kafka 到 Redis:端到端精确一次为什么难
很多人在写到 Flink 的 exactly-once 时只看引擎本身,但实际面试官往往会追问一句:“你保证的是引擎内的精确一次,还是端到端的精确一次?”答案肯定是后者更难。引擎内你可以靠 checkpoint 保住状态,但外部系统不受你事务管理,写入 Redis 的 key 一旦重复 set,就覆盖了。
在做实时开发时,我最常用的“兜底方案”是幂等设计。具体来说有三种常见套路:
- 唯一键约束:写入数据库时,用事件 ID(比如订单号 + 动作类型 + 时间戳)作为唯一键,重复插入会被数据库拒绝。
- Redis 事务 / Lua:用 SETNX 这种方式保证同一个 key 只被写入一次。
- 业务天然幂等:比如统计累计值,重复累加会导致数据翻倍,这时候就必须做额外去重。
笔试中如果你能把“引擎内精确一次”和“外部系统幂等”两个层次分开讲,再配合一两个真实案例,这个答案就比单纯背概念高一个档次。
4.3 去重、乱序和数据倾斜:笔试里最容易翻车的三件事
除了精确一次,2018 年校招笔试里几乎必考数据倾斜。实时计算里数据倾斜的典型场景是热点 key:大促时某个爆款商品的访问量是普通商品的上百倍,如果按商品 ID 做 keyBy,那么处理该商品的算子就会成为瓶颈,其他算子全部闲置,整个作业背压。
常见的解决思路包括:
- 把热点 key 做加盐(加随机后缀),分摊到多个子任务,最后再做一层聚合。
- 如果是双流 join,可以把大表广播(Broadcast),避免按 key shuffle。
- 对流量进行预聚合,在算子内部先做部分聚合,降低 shuffle 数据量。
我当时自己也踩过类似的坑:线上一个实时大屏作业,某个主播的销售额占了 40%,结果那个算子 CPU 打满,其余节点负载不到 10%,整个作业的延迟从 3 秒飙升到 30 秒。后来就是加了加盐策略,把热点主播的 key 拆成 64 个分片,聚合的时候再合并,问题才解决。面试时讲这种真实案例,比背一百个概念都管用。
5. 大促场景的高并发设计:背压、并行度和资源预估
5.1 背压不是报错,而是系统在喊“我撑不住了”
背压(Backpressure)是实时计算里非常核心的概念。简单类比:上游是水管,下游是水桶,水桶满了,水管就得减速,否则水会溢出来。Flink 的背压机制是自适应的:下游处理不过来时,会通过网络层反馈给上游,形成一种从下游到上游的减速信号。Storm 里处理背压要手动设计,Spark Streaming 因为微批有天然的缓冲,Flink 则是在网络传输层内置了流控协议。
笔试时如果问“实时任务延迟越来越高,可能是什么原因”,背压排查一定是第一条思路。我当时常做的定位方式是:
- 查看 Flink Web UI 的 Backpressure 面板,看哪些节点处于 High 状态。
- 如果只有某个节点 High,大概率是数据倾斜或者单点计算瓶颈。
- 如果所有节点都 High,说明整个作业的计算能力已经到达上限,需要扩容。
一定要记住,背压不是 bug,它只是系统自我保护的一种方式。你在答题时说出“背压是流量控制和资源分配问题”,会让面试官觉得你有实战手感。
5.2 并行度设置背后的水桶效应
并行度设多少?这是校招笔试里很容易被追问的问题,也是实际项目中最常被忽略的问题。有些同学会天真地回答“越大越好”,但并行度不是越高越快,它会带来三个副作用:
- 状态存储膨胀:每个并行子任务都要维护自己的状态,并行度高,checkpoint 的数据量和恢复时间都会变大。
- 网络 shuffle 开销增大:并行度翻倍,节点间数据传输量通常会增加一个量级。
- Kafka 分区阈值的限制:source 的并行度最高不能超过 Kafka Topic 的分区数。你把 source 并行度设成 64,但 Kafka 只有 10 个分区,那剩下的 54 个并行子任务完全是空转。
合理的并行度设置,需要结合数据量、单条消息处理时间和下游写入能力综合判断。比如一个消费 Kafka 的作业,单并行度每秒能处理 1 万条,而 Topic 每秒进来 10 万条,那 source 端至少 10 个并行度才够,同时还要考虑下游数据库的写入上限,不然你消费再快也会被写入瓶颈拖住。
5.3 资源预估的简单数学模型
2018 年的笔试题里有一道让我印象很深的题:大促预估峰值流量为每秒 20 万事件,单个事件的序列化和处理开销约为 1ms,需要多少个计算节点才能满足端到端延迟小于 5 秒?
这种题不需要精确答案,核心是你要有“估算”的意识。我当时算的逻辑大致如下:
- 20 万事件/秒 × 1ms/事件 = 200 秒的 CPU 处理时间(单核)。
- 要达到每秒处理完 20 万事件,需要约 200 核的并行计算能力。
- 假设每台机器 32 核,大约需要 7 台机器做计算。
- 再考虑 30%-50% 的冗余,所以 9-10 台机器比较稳妥。
这种估算方法虽然粗糙,但非常能体现你对实时系统的资源感官。笔试或面试中答出这个思路,面试官会知道你不只是会写代码,还能从容量规划角度思考问题。
6. 复盘 2018 真题,给准备实时开发岗位的人一些实在建议
6.1 刷题之外,真正该练的三个能力
如果你是在准备实时开发岗位的校招,我特别想在最后跟你分享:不要只盯着题海战术,有三个能力是做题刷不出来的。
第一,链路思维。拿到一个实时需求,你要能完整画出:数据从客户端埋点产生,经过日志采集、Kafka、实时计算引擎、最终写入 Redis/MySQL/ClickHouse 的全过程。笔试中很多问题都是链路中某一环的放大版。你如果把链路画明白,很多题目不用背也能推导出来。
第二,排查能力。真实面试场景里,面试官可能会抛出一个“线上问题”,比如:实时大屏某个指标突然变成 0,你怎么排查?这时候你要有一条清晰的排查链路:“先看上游数据有没有到 Kafka,再看消费位点有没有推进,再看作业日志有没有报错,再看下游写入是否被拒”。这条链路,比任何知识点都值钱。
第三,权衡能力。实时计算很多时候没有完美的答案,引擎选型、窗口长度、水位线设置、状态存储选择,每个决策都是在延迟、吞吐、准确性三者之间的权衡。面试官想听的不是标准答案,而是你面对多约束条件时能不能结构性地分析。
6.2 面试官想听到的答案长什么样
结合我后来参与校招面试的经历,我体会到面试官最反感两类答案:
一类是死记硬背的文章式答案,比如问“什么是水位线”,直接把 Flink 官方文档背一遍,没有任何业务场景的延展。另一类是“就事论事”的浅层答案,比如问“延迟高怎么办”,只回答“调并行度”,但说不清楚为什么调并行度有效、调到多大、有什么代价。
比较有竞争力的答案往往是“结构化的”。比如面对“如何保证实时统计的准确性”这种问题,你可以分成三个层次来答:
- 引擎层:用事件时间 + 水位线处理乱序,用 checkpoint 做故障恢复。
- 存储层:写入 Redis/MySQL 时,用幂等键或事务保证重复写入不翻倍。
- 业务层:明确业务对“准确性”的定义,是精确值还是误差可接受,再反向决定技术方案。
这种回答方式表明你不只是会单点操作,而是能从全局视角设计系统。
6.3 这套思路今天还能用吗
可能你会觉得,2018 年的真题放到今天是不是过时了?我的看法是,具体的 API 和引擎版本确实变了,但那套底层原理完全不过时。现在 Flink 越来越流行,数仓也往实时数仓 Lambda/Kappa 架构演进得更成熟了,但电商实时场景面临的本质问题依然是:数据乱序怎么处理、状态怎么管、精确一次怎么做、热点怎么打散、延迟和吞吐怎么平衡。你在任何一家公司做实时开发,每年双十一前都会重新面对这些问题。
所以如果你真的想走实时开发这条方向,不要只盯着“今年考什么新框架”,而要把那套底层逻辑彻底想透。框架会不断换代,原理不会。我见过太多人,学了 Flink 一堆 API,但一问到“为什么窗口函数要这样设计”“为什么 Flink 不需要 Spark 那套 cache”,就哑口无言了。反过来,那些把原理吃透的人,无论引擎怎么变,都能很快迁移。
我自己复习时最喜欢做的事,就是拿一张白纸,把一条消息从用户点击到最终看板展示的完整生命周期画下来,每一环都问自己“如果这里挂了怎么办”“如果这里慢了怎么办”。画到哪儿卡住,哪儿就是你的薄弱点。你要是能把这个动作重复画三遍,再去做任何一家公司的实时开发笔试题,心里都会踏实很多。