目录
值状态(ValueState)
Flink状态保留一个值,例如Integer、Long类型。
常用的方法如下
value() // 获取当前状态的值
update() // 更新当前状态的值
下面实现一个简单案例,来加深理解。watersensor对象的 ts 表示水位值,现在需要找到连续两条水位值相差大于10的数据,这时候就可以用值状态来存储上一条水位值。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
sensorDS
.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
// TODO 1.定义状态
ValueState<Integer> lastValueState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// TODO 2.在open方法中,初始化状态
// 状态描述器两个参数:第一个参数,起个名字,不重复;第二个参数,存储的类型
lastValueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("valueState", Types.INT));
}
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 取出上一条水位数据(存储在值状态中)
int lastVc = lastValueState.value() == null ? 0 : lastValueState.value();
Integer vc = waterSensor.vc;
if(Math.abs(lastVc-vc) > 10){
collector.collect("传感器:" + context.getCurrentKey() + "当前水位值" + vc + ",上一条水位值" + lastVc + "相差超过10");
}
lastValueState.update(vc);
}
})
.print();
env.execute();
}
由于使用按键分区状态,可以看到,s2,11,11这条数据,他的上一条tc应该为null,值状态也为空,代码中手动赋值为0,因此这条数据也被打印输出。因此,也证明了每一个key都有一个状态。
列表状态(ListState)
将需要保存的值,以列表的形式存储起来。
常用方法如下
ListState.get(); //取出 list状态 本组的数据,是一个Iterable
ListState.add(); // 向 list状态 本组 添加一个元素
ListState.addAll(); // 向 list状态 本组 添加多个元素
ListState.update(); // 更新 list状态 本组数据(覆盖)
ListState.clear(); // 清空List状态 本组数据
下面实现一个案例,输出每个传感器(id)最高的三个水位值。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
// 输出每个传感器最高的3个水位值
sensorDS.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
ListState<Integer> listState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
listState = getRuntimeContext().getListState(new ListStateDescriptor("vclistState", Types.INT));
}
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 1、来一条数据存储一条
listState.add(waterSensor.vc);
// 2、从list状态拿出来(Iterable), 拷贝到一个List中,排序, 只留3个最大的
Iterable<Integer> iter = listState.get();
List<Integer> list = new ArrayList<>();
for (Integer vc : iter) {
list.add(vc);
}
// 降序,后 - 前
list.sort((o1,o2) -> (o2-o1));
// 3、只保留最大的3个(list中的个数一定是连续变大,一超过3就立即清理即可)
if(list.size() > 3){
list.remove(3);
}
collector.collect("传感器id为" + waterSensor.getId() + "最大的三个水位值为" + list.toString());
listState.update(list);
}
})
.print();
env.execute();
}
Map状态(MapState)
将状态以键值对的形式存储。
常用方法如下:
vcCountMapState.get(); // 对本组的Map状态,根据key,获取value
vcCountMapState.contains(); // 对本组的Map状态,判断key是否存在
vcCountMapState.put(, ); // 对本组的Map状态,添加一个 键值对
vcCountMapState.putAll(); // 对本组的Map状态,添加多个 键值对
vcCountMapState.entries(); // 对本组的Map状态,获取所有键值对
vcCountMapState.keys(); // 对本组的Map状态,获取所有键
vcCountMapState.values(); // 对本组的Map状态,获取所有值
vcCountMapState.remove(); // 对本组的Map状态,根据指定key,移除键值对
vcCountMapState.isEmpty(); // 对本组的Map状态,判断是否为空
vcCountMapState.iterator(); // 对本组的Map状态,获取迭代器
vcCountMapState.clear(); // 对本组的Map状态,清空
实现一个具体的案例,统计每种传感器不同水位线出现的次数
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
sensorDS
.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
// Map<vc,count>
MapState<Integer,Integer> mapState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<Integer, Integer>(
"mapState",
Types.INT,
Types.INT
));
}
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 判断是否存在key
Integer vc = waterSensor.getVc();
if(mapState.contains(vc)){
// 存在,则修改count值
Integer count = mapState.get(vc);
mapState.put(vc,++count);
}else{
// 不存在,则放入mapState
mapState.put(vc,1);
}
// 遍历Map状态,输出每个k-v的值
StringBuilder result = new StringBuilder();
result.append("================\n");
result.append("传感器id为" + waterSensor.getId() + "\n");
for (Map.Entry<Integer, Integer> entry : mapState.entries()) {
// entries 返回完整的键值对数据,可以直接toString
result.append(entry.toString() + "\n");
}
collector.collect(result.toString());
}
})
.print();
env.execute();
}
归约状态(ReducingState)
类似值状态,对于添加进来的所有数据进行规约,结果作为规约状态保存起来。调用add方法时,不是将数据存储到状态中,而是将规约计算的结果存储到状态里。
常用方法如下:
vcSumReducingState.get(); // 对本组的Reducing状态,获取结果
vcSumReducingState.add(); // 对本组的Reducing状态,添加数据
vcSumReducingState.clear(); // 对本组的Reducing状态,清空数据
实现案例:计算每种传感器的水位和
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
sensorDS
.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
ReducingState<Integer> reducingState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
reducingState = getRuntimeContext().getReducingState(
new ReducingStateDescriptor<Integer>(
"reducingState",
new ReduceFunction<Integer>() {
@Override
public Integer reduce(Integer integer, Integer t1) throws Exception {
return integer + t1;
}
},
Types.INT
)
);
}
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 来一条添加一条
reducingState.add(waterSensor.getVc());
Integer sum = reducingState.get();
collector.collect("传感器id为" + waterSensor.getId() + ",其水位和为" + sum);
}
})
.print();
env.execute();
}
聚合状态(AggregatingState)
类似于规约状态,但状态以累加器的形式进行存储。
常用方法如下:
vcAvgAggregatingState.get(); // 对 本组的聚合状态 获取结果
vcAvgAggregatingState.add(); // 对 本组的聚合状态 添加数据,会自动进行聚合
vcAvgAggregatingState.clear(); // 对 本组的聚合状态 清空数据
示例:计算每个传感器的平均水位值
这里聚合状态内存储累加器 Tuple2<sum,count>,但是在定义聚合状态的时候,泛型为<IN,OUT>,定义描述器时,泛型为<IN,AGG,OUT>。而在processElement时,add传入IN类型,通过get状态返回值为OUT类型。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
sensorDS
.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 将 水位值 添加到 聚合状态中
aggregatingState.add(waterSensor.getVc());
// 从 聚合状态中 获取结果
Double result = aggregatingState.get();
collector.collect("传感器id为" + waterSensor.getId() + ",平均水位值为" + result);
}
// AGG : Tuple2<sum,count>
// OUT : avg
AggregatingState<Integer,Double> aggregatingState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
aggregatingState = getRuntimeContext().getAggregatingState(
// IN, ACC, OUT
new AggregatingStateDescriptor<Integer, Tuple2<Integer,Integer>,Double>(
"aggregatingState",
new AggregateFunction<Integer, Tuple2<Integer, Integer>, Double>() {
@Override
public Tuple2<Integer, Integer> createAccumulator() {
return Tuple2.of(0,0);
}
@Override
public Tuple2<Integer, Integer> add(Integer integer, Tuple2<Integer, Integer> agg) {
return Tuple2.of(agg.f0+integer,agg.f1+1);
}
@Override
public Double getResult(Tuple2<Integer, Integer> agg) {
// 进行简单的类型转换,int/int返回都变了会报错
return agg.f0 * 1D / agg.f1;
}
@Override
public Tuple2<Integer, Integer> merge(Tuple2<Integer, Integer> integerIntegerTuple2, Tuple2<Integer, Integer> acc1) {
return null;
}
},
Types.TUPLE(Types.INT,Types.INT)
));
}
}
)
.print();
env.execute();
}
以上五种都是按键分区状态,还有一种状态叫算子状态,这里介绍一下广播状态。
广播状态(BroadcastState)
广播状态以map形式存在,key可以是配置名称,value为具体的值
实现案例:水位超过指定的阈值发送告警,阈值可以动态修改
这里直接通过调用broadcast方法即可,传一个MapStateDescriptor,key为阈值,value为设置的具体值。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
SingleOutputStreamOperator<WaterSensor> sensorDS = env.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction());
// 配置流(用来传配置)
DataStreamSource<String> configDS = env.socketTextStream("node1", 8888);
// TODO 1. 将 配置流 广播
// new MapStateDescriptor<>()
MapStateDescriptor<String, Integer> broadcastState = new MapStateDescriptor<>("broadcast-state", Types.STRING, Types.INT);
BroadcastStream<String> configBS = configDS.broadcast(broadcastState);
// TODO 2.把 数据流 和 广播后的配置流 connect
BroadcastConnectedStream<WaterSensor, String> connect = sensorDS.connect(configBS);
// TODO 3.调用 process
connect
.process(
new BroadcastProcessFunction<WaterSensor, String, String>() {
@Override
public void processElement(WaterSensor waterSensor, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
// TODO 5.通过上下文获取广播状态,取出里面的值(只读,不能修改)
ReadOnlyBroadcastState<String, Integer> broadcastState2 = readOnlyContext.getBroadcastState(broadcastState);
Integer threshold = broadcastState2.get("threshold");
threshold = threshold==null?0:threshold;
if (waterSensor.vc > threshold){
collector.collect(waterSensor + ",水位超过阈值:" + threshold);
}
}
@Override
public void processBroadcastElement(String s, Context context, Collector<String> collector) throws Exception {
// TODO 4. 通过上下文获取广播状态,往里面写数据
// key统一设置为阈值
BroadcastState<String, Integer> broadcastState1 = context.getBroadcastState(broadcastState);
broadcastState1.put("threshold",Integer.valueOf(s));
}
}
)
.print();
env.execute();
}
注意两个流传入数据的先后数据,如果配置流后传数据,就没有一个初始的阈值,此时可以将阈值加个null判断设置为0。这里也使用了新的process方法,BroadcastProcessFunction,基于没有keyby过的连接流调用,分别处理数据流和广播流的数据。
需要注意的是,广播状态在process方法外申明,要使用需要通过上下文对象取得,只有广播流的processElement方法可以修改状态值,对数据流是只读状态。
状态生存时间(TTL)
在实际生产中,状态需要即使的清除,某些场景不适合调用clear,因此flink支持设置状态生存时间,其设置代码如下。
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// TODO 2.创建 StateTtlConfig
StateTtlConfig ttl = StateTtlConfig
// 设置过期时间为5s
.newBuilder(Time.seconds(5))
// 创建或写操作 更新
.updateTtlOnCreateAndWrite()
// 不返回过期的状态值
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
// TODO 3.状态描述器 启用 TTL
ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("valueState", Types.INT);
valueStateDescriptor.enableTimeToLive(ttl);
// TODO 4.在open方法中,初始化状态
// 状态描述器两个参数:第一个参数,起个名字,不重复;第二个参数,存储的类型
lastValueState = getRuntimeContext().getState(valueStateDescriptor);
}
例如使用上面的示例,TTL过期时间为5s,只有创建或者执行write操作时更新,意思是:调用process方法的时候,会更新一个TTL,只要接下来在5s内写入数据,生存时间又会更新为5s,状态值仍然存在,但间隔大于5s了,状态值会重新new一个。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("node1", 7777)
.map(new WaterSensorMapFunction())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((value,ts) -> value.getTs()*1000L)
);
sensorDS
.keyBy(value -> value.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
// 取出上一条水位数据(存储在值状态中)
int lastVc = lastValueState.value() == null ? 0 : lastValueState.value();
Integer vc = waterSensor.vc;
if(Math.abs(lastVc-vc) > 10){
collector.collect("传感器:" + context.getCurrentKey() + "当前水位值" + vc + ",上一条水位值" + lastVc + "相差超过10");
}
lastValueState.update(vc);
}
// TODO 1.定义状态
ValueState<Integer> lastValueState;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// TODO 2.创建 StateTtlConfig
StateTtlConfig ttl = StateTtlConfig
// 设置过期时间为5s
.newBuilder(Time.seconds(5))
// 创建或写操作 更新
.updateTtlOnCreateAndWrite()
// 不返回过期的状态值
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
// TODO 3.状态描述器 启用 TTL
ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("valueState", Types.INT);
valueStateDescriptor.enableTimeToLive(ttl);
// TODO 4.在open方法中,初始化状态
// 状态描述器两个参数:第一个参数,起个名字,不重复;第二个参数,存储的类型
lastValueState = getRuntimeContext().getState(valueStateDescriptor);
}
})
.print();
env.execute();
}
图中事件事件为11和12的数据其实在手动输入时,间隔了超过5秒,因此在12数据执行processElement方法时,他的上一条vc值为null,差值大于10,输出。
状态后端
分为HashMapStateBackend和RocksDB,前者存储在内存里(TaskManager的JVM上),后者存储在本地硬盘,写入时需要序列化。
显然,两者的读写性能hash快很多,但是要考虑一点,状态的数据量很大的时候,如果使用哈希表状态后端,TaskManager的内存不一定够用,这时候就使用RocksDS,把状态持久化到硬盘。