生而为人

程序员的自我修养

0%

函数

[toc]

Map[String, () => Solution]

以函数作为map值存储的

在 Scala 中,Map[String, () => Solution] 里的 () => Solution 是一个函数类型,它表示一个不接受任何参数、返回一个 Solution 对象的函数。将这样的函数作为 Map 的值存储,体现了 函数作为一等公民(first-class citizen) 的特性,即函数可以像普通值一样被传递、存储和调用。

具体来说,这里的 () => Solution 是一个函数值(function value),它是函数类型的实例。你可以把它看作是一个工厂:当你需要一个新的 Solution 实例时,就调用这个函数(通过 apply() 或直接加括号 ()),它会生成一个实例。这种存储函数而不是直接存储实例的做法,本质上是在存储一个生产实例的行为,而不是实例本身。

这种设计模式常被称为工厂函数存储延迟初始化,它是函数式编程中常见的技巧,利用了函数作为值的灵活性。

函数的返回值

根据返回值数量选择合适的数据类型

固定数量、相同类型 → 元组 / case class

可变数量、相同类型 → 集合(List、Seq、Vector 等)

可变数量、不同类型 → 用 sealed trait 和集合模拟,或使用 HList(shapeless)

两种可能的结果 → Either、Try、Option

当返回值可能成功也可能失败时:使用 Either 或 Try

如果你的函数可能返回多种结果(例如成功时返回一个值,失败时返回错误信息),可以使用 Either

1
2
3
def divide(a: Int, b: Int): Either[String, Int] = 
if (b == 0) Left("Division by zero")
else Right(a / b)

Either 通常用于表示两种可能的结果,但你可以嵌套组合来表示更多情况。

富函数

为什么可以通过富函数类来自定义按键分区状态

在 Flink 中,富函数(RichFunction) 能够自定义按键分区状态(Keyed State),是因为 Flink 的设计将 “运行时上下文(RuntimeContext)”“状态后端(State Backend)” 无缝集成到了富函数的生命周期中。

简单来说:富函数充当了用户逻辑与 Flink 分布式状态管理机制之间的桥梁。下面从几个关键点解释其原理。


1. 富函数提供 RuntimeContext,这是状态管理的入口

当你在 KeyedStream 上使用富函数(如 RichFlatMapFunctionKeyedProcessFunction)时,Flink 会在算子初始化时通过 open() 方法将 RuntimeContext 注入到函数实例中。

RuntimeContext 提供了以下关键方法,用于创建和管理按键分区状态:

  • getState(ValueStateDescriptor)
  • getListState(ListStateDescriptor)
  • getMapState(MapStateDescriptor)
  • getReducingState(ReducingStateDescriptor)
  • getAggregatingState(AggregatingStateDescriptor)

这些方法返回的状态对象(如 ValueState)会自动绑定到当前处理的数据的 key 上。


2. 按键分区状态的自动 key 感知

按键分区状态的核心特点是:每个不同的 key 都有自己独立的状态副本。富函数在 KeyedStream 上使用时,Flink 的运行时框架会自动将当前处理的元素的 key 与状态对象关联

具体流程:

  • 当数据进入算子时,Flink 知道这条数据属于哪个 key(通过 KeySelector 或分区方式确定)。
  • 在调用 processElement() 等方法时,RuntimeContext 内部会根据当前 key 找到对应的状态分区。
  • 用户代码通过状态对象读写时,实际上操作的是当前 key 对应的状态,无需手动管理 key 到状态的映射。

这种机制被称为 “按键分区状态的自动作用域(scoping)”


3. 状态后端负责实际存储和容错

用户通过富函数定义的状态描述符(StateDescriptor)只声明了状态的名称、类型、序列化器以及可选的TTL(Time-To-Live)。真正的状态数据由 状态后端(State Backend) 管理:

  • **MemoryStateBackend**:状态存储在 JVM 堆内存中,适合本地测试。
  • **FsStateBackend**:状态存储在内存中,但检查点会持久化到文件系统。
  • **RocksDBStateBackend**:状态存储在本地 RocksDB 数据库中,支持超大状态。

无论使用哪种状态后端,富函数中的状态操作都通过统一的 State 接口访问,Flink 会在后台处理序列化、缓存、磁盘读写、检查点快照等复杂逻辑。


4. 容错与一致性保证

富函数中的按键分区状态会自动参与检查点(Checkpoint) 机制。当作业失败恢复时,Flink 会从最近成功的检查点中恢复所有 key 的状态,并将其重新加载到对应的算子实例中。这一过程对用户代码完全透明,只需在富函数中定义状态即可。


5. 设计优势:为什么不用普通函数?

如果不使用富函数,而是自己维护一个 Map<Key, Value> 来实现类似功能,会遇到以下问题:

  • 内存溢出:状态无法自动溢写到磁盘。
  • 故障恢复困难:需要手动实现状态的快照和恢复。
  • 无法感知 key 的变化:需要手动从数据中提取 key 并维护映射。
  • 性能差:缺少 Flink 内部的优化(如状态缓、增量快照、异步检查点等)。

富函数 + 按键分区状态的设计,将分布式状态管理的复杂性完全封装,让用户能像写单机程序一样使用状态,同时享受 Flink 提供的高性能、一致性和容错能力。


💎 总结

组件 作用
富函数 提供生命周期方法(openclose)和运行时上下文(RuntimeContext)。
RuntimeContext 状态创建入口,自动绑定当前 key。
StateDescriptor 声明状态的名称、类型、序列化器、TTL 等元信息。
状态后端 实际存储状态数据,支持内存、文件系统、RocksDB,并负责检查点容错。

因此,通过富函数自定义按键分区状态,本质上是 Flink 将分布式状态管理抽象为易用的编程模型,让开发者无需关心底层的 key 路由、状态分区、容错快照等细节。

富函数只能在KeyedStream上使用吗

不是的,富函数(RichFunction)并不仅限于 KeyedStream,它可以在任何数据流上使用

几乎所有 Flink 算子(如 map, filter, flatMap 等)都同时提供了普通函数和对应的富函数版本,例如 RichMapFunction, RichFilterFunction, RichFlatMapFunction-20-8。即使是在非按键分区的普通数据流(DataStream)上,你依然可以无缝地使用这些富函数-。


🧩 富函数在非KeyedStream上的主要应用

虽然不能使用键控状态(Keyed State),但在非KeyedStream上,富函数依然提供了两个非常重要的能力:

  • **生命周期管理 (Lifecycle Management)**:RichFunction 提供了 open()close() 两个生命周期方法-36
    • open()**:在算子的实际工作方法(如 map())被调用之前执行一次,常用于一次性初始化**任务,如建立数据库连接、加载配置文件等-20
    • close()**:在算子生命周期结束时调用,用于执行清理工作**,如关闭连接、释放资源等-20
  • 获取运行时上下文 (Access to RuntimeContext)**:通过 getRuntimeContext() 方法,可以获取算子的运行时信息,例如算子并行度、任务名称**等-36。这为开发通用、自适应的函数提供了基础。

🚫 在非KeyedStream上的能力限制

在非 KeyedStream 上使用富函数时,确实无法访问以下两项关键功能:

  • **无法访问键控状态 (Keyed State)**:键控状态是基于 Key 进行分区的,由于 DataStream 没有经过 keyBy() 分区,因此无法获取和使用 ValueState, ListState 等键控状态--5
  • **无法访问定时器服务 (TimerService)**:TimerService(如 registerEventTimeTimer())依赖于为特定 Key 注册定时器的能力,因此在非 KeyedStream 上不可用--1

💎 总结

  • 能否使用? 可以,富函数可以用在任何数据流上。
  • 与 KeyedStream 的区别?KeyedStream无法使用键控状态和定时器服务-2,但可以正常使用生命周期方法和运行时上下文。