Distributed Cache为我们提供了一种扩展数据的方案,但有些时个并不能满足需求,如我们有一个MySql表中存储了部分一些字典数据,并且它可能随时更新,这时我们需要动态感知其变化(近实时)来对数据进行计算。
这时可以使用一个通用的做法:将小"表"广播出去。以下是我们运行类all.in.one.c06.Chapter06
时,WebUI给出的图:
其主要的实现逻辑在
someDataStream
.connect(toBroadcastStream.broadcast(descriptor))
.process(new C06BroadcastProcessFunction(descriptor))
.print()
将toBroadcastStream广播出去,使用connect将两个流连接,会得到一个BroadcastConnectedStream
,然后分别处理当数据流和广播流数据到达时的逻辑即可。
在处理广播数据时,使用了方法ctx.getBroadcastState
,它返回的是BroadcastState
。它专为广播设计的,实现了Flink中的State接口。
State
State是Flink在流计算过程中,计算节点的中间计算结果或元数据属性,如聚合过程中计算中间聚合结果、Source为Kafka时记录offset等。官方说明即:State就是与时间相关的,Apache Flink任务的内部数据(计算数据和元数据属性)的快照。
State主要有两大作用:
增量计算:流计算中,计算场景大多数是增量计算,我们要基于上一次计算的结果之上进行处理,State提供了计算数据的存储能力(持久化)
Failover:在出现机器、网络、脏数据等等原因出现程序错误任务重启时,为了保证我们可以重新恢复现场,需要State提供的数据持久化能力支持(Checkpoint)
简单介绍下Flink提供的四种常用state存储实现:
基于内存的HeapStateBackend: 在debug模式使用,不建议在生产模式下应用
基于HDFS的FsStateBackend: 分布式文件持久化,每次读写都产生网络IO,整体性能不佳
基于RocksDB的RocksDBStateBackend: 本地文件+异步HDFS持久化,生产环境常用
基于Niagara(Alibaba内部实现)NiagaraStateBackend: 分布式持久化,在Alibaba生产环境应用
除了上面提到的BroadcastState
,State通常按以下方式划分:
KeyedState:一般与KeyingBy产生的Keyed Stream配合使用,即每个key可以有它自己的值
ValueState<T>
: 存储单值ListState<T>
: 存储列表ReducingState<T>
: 保存单值,但可以定义一个ReduceFunction来表示和原来已经存在的值进行何种方式的聚合AggregatingState<IN, OUT>
: 保存单值,与ReducingState唯一不同在于聚合后的值可能类型不同MapState<UK, UV>
: 保存映射列表
OperatorState: 定义Source时,保存source读取的位置(如kafka的offset)
package all.in.one.c06 import all.in.one.utils.random import org.apache.flink.api.common.state.MapStateDescriptor import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction import org.apache.flink.streaming.api.functions.source.SourceFunction import org.apache.flink.streaming.api.scala._ import org.apache.flink.util.Collector import org.joda.time.DateTime object Chapter06 extends App { val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI() // 定义要广播的stream val toBroadcastStream = env.addSource(new SourceFunction[(String, String)] { var running = true override def run(ctx: SourceFunction.SourceContext[(String, String)]): Unit = do { val now = DateTime.now() val state = if (now.getSecondOfMinute % 2 == 0) "even" else "odd" val timeStr = now.toString("HH:mm:ss.SSS") ctx.collect(state -> timeStr) Thread.sleep(random.int(5000, 15000).toLong) } while (running) override def cancel(): Unit = running = false }) // 定义一个正常stream val someDataStream = env.addSource(new SourceFunction[Int] { var running = true override def run(ctx: SourceFunction.SourceContext[Int]): Unit = do { ctx.collect(1) Thread.sleep(1000L) } while (running) override def cancel(): Unit = running = false }) // 定义Descriptor val descriptor: MapStateDescriptor[String, String] = new MapStateDescriptor("C06 - broadcast", classOf[String], classOf[String]) // 将数据流与广播流连接 someDataStream .connect(toBroadcastStream.broadcast(descriptor)) .process(new C06BroadcastProcessFunction(descriptor)) .print() env.execute("Chapter 06") /** * 定义处理流的process function */ class C06BroadcastProcessFunction(mapStateDescriptor: MapStateDescriptor[String, String]) extends BroadcastProcessFunction[Int, (String, String), String] { /** * 当数据流数据到达的时候执行此方法处理 */ override def processElement( value: Int, ctx: BroadcastProcessFunction[Int, (String, String), String]#ReadOnlyContext, out: Collector[String] ): Unit = { val even = ctx.getBroadcastState(mapStateDescriptor).get("even") val odd = ctx.getBroadcastState(mapStateDescriptor).get("odd") val evenStr = if (even != null) s"even -> [$even]" else "" val oddStr = if (odd != null) s"odd -> [$odd]" else "" val toPrint = if (evenStr.isEmpty && oddStr.isEmpty) "无" else if (evenStr.isEmpty && oddStr.nonEmpty) oddStr else if (evenStr.nonEmpty && oddStr.isEmpty) evenStr else s"$evenStr, $oddStr" out.collect(toPrint) } /** * 当广播流数据到达的时候执行此方法处理 */ override def processBroadcastElement( value: (String, String), ctx: BroadcastProcessFunction[Int, (String, String), String]#Context, out: Collector[String] ): Unit = { // 将数据更新到state中 ctx.getBroadcastState(mapStateDescriptor).put(value._1, value._2) } } }
喜欢flink开发的可以加微信:vx(喜欢学习的来哈)
公众号回复:“资料全集”,海量PPT等你来拿。