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)
}
}
}