文:王东阳
前言
状态在Flink中叫作State,用来保存中间计算结果或者缓存数据。根据是否需要保存中间结果,分为无状态计算和有状态计算。对于流计算而言,事件持续不断地产生,如果每次计算都是相互独立的,不依赖于上下游的事件,则是无状态计算。如果计算需要依赖于之前或者后续的事件,则是有状态计算。
在Flink中根据数据集是否根据Key进行分区,将状态分为Keyed State和Operator State(Non-keyed State)两种类型 ,本文主要介绍Keyed State,后续文章会介绍Operator State。
Keyed State 表示和key相关的一种State,只能用于KeydStream类型数据集对应的Functions和Operators之上。Keyed State是Operator State的特例,区别在于Keyed State事先按照key对数据集进行了分区,每个Key State仅对应一个Operator和Key的组合。Keyed State可以通过Key Groups进行管理,主要用于当算子并行度发生变化时,自动重新分布 Keyed State数据。在系统运行过程中,一个Keyed算子实例可能运行一个或者多个Key Groups的keys。
按照数据结构的不同,Flink中定义了多种Keyed State,具体如下:
ValueState<T>
即类型为T的单值状态。这个状态与对应的Key绑定,是最简单的状态。ListState<T>
即Key上的状态值为一个列表。MapState<UK,UV>
定义与Key对应键值对的状态,用于维护具有key-value结构类型的状态数据。ReducingState<T>
这种State通过用户传入的ReduceFucntion,每次调用add(T)
方法添加元素时,会调用ReduceFucntion,最后合并到一个单一的状态值。AggregatingState<IN,OUT>
聚合State和ReducingState<T>
非常类似,不同的是,这里聚合的类型可以是不同的元素类型,使用add(IN)
来加入元素,并使用AggregateFunction函数计算聚合结果。
State开发实战
本章节将通过实际的项目代码演示不同数据结构在不同计算场景下的开发方法。
本文中项目 pom文件
ValueState
ValueState<T>
即类型为T的单值状态。这个状态与对应的Key绑定,是最简单的状态。可以通过update(T)
方法更新状态值,通过T value()
方法获取状态值。
由于ValueState<T>
是与Key对应单个值的状态,应用场景可以是例如统计user_id对应的交易次数,每次用户交易都会在count状态值上进行更新。
接下来我们将通过一个具体的实例代码展示如何利用ValueState去统计各个用户的交易次数。
ValueStateCountUser实现
通过定义ValueState<Integer>
类型的countState存储用户已经访问的次数,并在每次收到新的元素时,对countState中的数据加1更新,同时把当前用户名和已经访问的总次数返回。在本样例代码中将会通过 RichMapFunction 来对流元素进行处理。
- 在其中的
open(Configuration parameters)
方法中对 countState进行初始化。Flink提供了RuntimeContext
用于获取状态数据,同时RuntimeContext
提供了常用的Managed Keyd State的获取方式,可以通过创建相应的StateDescriptor并调用RuntimeContext方法来获取状态数据。例如获取ValueState可以调用ValueState[T] getState(ValueStateDescriptor[T])
方法。 - 在
Tuple2<String, Integer> map(Tuple2<String, String> s)
中,通过countState.value()
获取用户之前的访问次数,然后对countState中的数据加1更新,同时把当前用户名和已经访问的总次数返回。
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
public class ValueStateCountUser extends RichMapFunction<Tuple2<String, String>, Tuple2<String, Integer>> {
private ValueState<Integer> countState;
public ValueStateCountUser() {
}
@Override
public void open(Configuration parameters) throws Exception {
// super.open(parameters);
countState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("count", Integer.class));
}
@Override
public Tuple2<String, Integer> map(Tuple2<String, String> s) throws Exception {
Integer count = countState.value();
if (count == null) {
countState.update(1);
return Tuple2.of(s.f0, 1);
}
countState.update(count + 1);
return Tuple2.of(s.f0, count + 1);
}
@Override
public void close() throws Exception {
// super.close();
System.out.println(String.format("finally %d", countState.value()));
}
}
验证代码
编写验证代码如下
private static void valueStateUserCount() throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStream<Tuple2<String, String>> inputStream = env.fromElements(
new Tuple2<>("zhangsan","aa"),
new Tuple2<>("lisi","aa"),
new Tuple2<>("zhangsan","aa"),
new Tuple2<>("lisi","aa"),
new Tuple2<>("wangwu","aa")
);
inputStream.keyBy(new KeySelector<Tuple2<String, String>, String>() {
@Override
public String getKey(Tuple2<String, String> integerLongTuple2) throws Exception {
return integerLongTuple2.f0;
}
})
.map(new ValueStateCountUser())
.print();
env.execute("StateCompute");
}
代码地址:valueStateUserCount
程序运行得到输出如下
(zhangsan,1)
(lisi,1)
(zhangsan,2)
(lisi,2)
(wangwu,1)
finally 1
可以看到对于流数据中的每一个元素,根据用户名进行统计,实时显示该用户已经访问了多少次。
ListState
ListState<T>
即Key上的状态值为一个列表。可以通过add(T)
方法或者addAll(List[T])
往列表中附加值;也可以通过Iterable get()
方法返回一个Iterable<T>
来遍历状态值;使用update(List[T])
来更新元素。
由于ListState保存的是与Key对应元素列表的状态,状态中存放元素的List列表,应用场景可以是例如定义ListState存储用户经常访问的IP地址。
接下来我们将通过一个具体的实例代码展示如何利用ListState去统计各个用户访问过的IP地址列表。为了更广泛的展示State的应用场景,本样例代码中将会基于KeyedProcessFunction
来处理流元素。
ListStateUserIP 实现
- 声明
ListState<String> ipState
用于记录用户访问过的ip地址 - 在
open(Configuration parameters)
中利用getRuntimeContext().getListState
对 ipState 进行初始化,getListState需要传入ListStateDescriptor类型的参数。 - 在
processElement
中,将当前记录中的IP地址追究到ipState
中,然后从ipState
获取已经访问过的所有IP地址,和用户名一起放入到Collector
import com.google.common.