6.8 Flink状态编程
有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。
6.8.1 Flink中的状态分类
Flink包括两种基本类型的状态Managed State和Raw State
Managed State | Raw State | |
---|---|---|
状态管理方式 | Flink Runtime托管, 自动存储, 自动恢复, 自动伸缩 | 用户自己管理 |
状态数据结构 | Flink提供多种常用数据结构, 例如:ListState, MapState等 | 字节数组: byte[] |
使用场景 | 绝大数Flink算子 | 所有算子 |
注意:
-
从具体使用场景来说,绝大多数的算子都可以通过集成Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State一般是在已有算子和Managed State不够用时,自定义算子时使用。
-
在我们平时的使用中,Managed State已经足够使用。
6.8.2 Managed State的分类
对Managed State继续细分,它又有两种类型:Keyed State(键控状态)、Operator State(算子状态)
Operator State | Keyed State | |
---|---|---|
适用用算子类型 | 可用于所有算子: 常用于source, 例如 FlinkKafkaConsumer | 只适用于KeyedStream上的算子 |
状态分配 | 一个算子的子任务对应一个状态 | 一个Key对应一个State: 一个算子会处理多个Key, 则访问相应的多个State |
创建和访问方式 | 实现CheckpointedFunction或ListCheckpointed(已经过时)接口 | 重写RichFunction, 通过里面的RuntimeContext访问 |
横向扩展 | 并发改变时有多重重写分配方式可选: 均匀分配和合并后每个得到全量 | 并发改变, State随着Key在实例间迁移 |
支持的数据结构 | ListState和BroadCastState | ValueState, ListState,MapState ReduceState, AggregatingState |
6.8.3 算子状态(Operator State)的使用
(1)算子状态特点
Operator State可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。
注意:算子子任务之间的状态不能互相访问。
Operator State的实际使用场景不如Keyed State多,它经常被用在Source和Sink等算子上,用来保留流入数据的偏移量或对输出数据做缓存,以保证Flink应用的Exactly-Once语义。
(2)算子状态三种基本数据结构:
发生故障时(并行度改变了),或者从保存点savepoint启动应用程序时数据如何恢复?
-
列表状态(List State)
将状态表示为一组数据的列表,当并行度发生变化,数据均匀分配给多个运行实例。
-
联合列表状态(Union List State)
将所有state合并为全量state,再分发给每个运行实例。
-
广播状态(Broadcast State)
1. 一个并行实例 一个状态
2. 自定义算子状态,要实现一个CheckpointedFunction接口,
实现两个方法snapshotState()和initializeState()
3. 算子状态的数据结构是一个listState:根据出错数据恢复的方式分为:
1) 普通的listState
2) 联合列表状态Union list State
public class Flink26_State_Operator {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(new MyCountMapper())
.print();
env.execute();
}
public static class MyCountMapper implements MapFunction<String, Long>, CheckpointedFunction{
private Long count = 0L;
private ListState<Long> state;
@Override
public Long map(String value) throws Exception {
count++;
return count;
}
/*
* @Description //TODO checkpoint时会调用这个方法,要实现具体的snapshot逻辑,比如将哪些本地状态初始化
* @Param [context]
* @return void
**/
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
System.out.println("snapshotState...");
state.clear();
state.add(count);
}
/*
* @Description //TODO 初始化,向本地状态中填充数据,每个子任务调用一次
* @Param [context]
* @return void
**/
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
System.out.println("initializeState...");
state = context.getOperatorStateStore()
.getListState(new ListStateDescriptor<Long>("state", Types.LONG));
for (Long c : state.get()) {
count += c;
}
}
}
}
6.8.4 键控状态(Keyed State)的使用
(1)键控状态特点
键控状态是根据输入数据流中定义的key来维护和访问的,也就是说一个key 一个state(一个分组一个状态)
(2)键控状态支持的数据类型
-
ValueState
保存单个值,每个key(分组)有一个状态
-
步骤1:定义 ValueState valueState;
-
步骤2:初始化,open()内 valueState = getRuntimeContext().getState(new ValueStateDescriptor(状态名,状态类型,初始值))
-
步骤3:操作状态
- 获取状态存储的值:valueState.value();
- 更新状态的值:update()
- 清空本组的状态值:clear()
-
-
ListState
保存list结构的多个值
-
MapState<UK, UV>
保存map结构
-
ReducingState
保存单个值,把所有元素的结果聚合,添加到状态中,输入和输出的类型必须一样
-
AggregatingState<IN, OUT>
保存单个值,把元素进行聚合。不同于reducingState的是AggregatingState的输入和输出类型可以不一样。
(3)键控流状态API
public class Flink27_State_Keyed {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
}
})
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() { // 指定如何从数据中提取事件时间
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L; // 转换成 毫秒
}
})
);
sensorDS
.keyBy(sensor -> sensor.getId())
// 键控状态: keyby之后
// 一个分组对应一个状态,分组之间是隔离的,跟并行度无关
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
//TODO 1 定义状态
ValueState<Integer> valueState;
ListState<WaterSensor> listState;
MapState<String, Long> mapState;
//TODO 2 在open方法里,初始化状态
@Override
public void open(Configuration parameters) throws Exception {
//TODO 3.1 ValueState
//从richFunction的运行时环境中获取状态,需要给状态起名字,指明类型,也可以设置初始值
valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("valueState", Types.INT));
//Integer状态的初始值是null,第三个参数是默认值。
// valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("valueState", Integer.class, 0));
//TODO 3.2 ListState
listState = getRuntimeContext().getListState(new ListStateDescriptor<WaterSensor>("listState", WaterSensor.class));
//TODO 3.3 MapState
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("mapState", Types.STRING, Types.LONG));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
// //TODO 4.1 ValueState
// valueState.value(); //获取状态存储的值
// valueState.update(1); //更新状态值
// valueState.clear(); //清空状态,注意,清空的是本组的状态,value是哪个组的,就清空哪个组
//
// //TODO 4.2 ListState
// Iterable<WaterSensor> waterSensors = listState.get(); //获取状态里的数据,返回一个可迭代类型
// listState.add(new WaterSensor("sensor_1", 1L, 1)); //添加单个值
// listState.addAll(); //添加整个list
// listState.update(); //更新整个list
// listState.clear(); //清空本组的状态
//
// //TODO 4.3 MapState
// mapState.get(); //通过某个key,获取对应的value
// mapState.put(); //添加一对key - value
// mapState.putAll(); //添加整个map
// mapState.contains(); //判断是否包含某个key
// mapState.remove(); //删除某个key对应的数据
// mapState.clear(); //清空本组的状态
}
})
.print("result");
env.execute();
}
}
(4)键控流状态案例1:获取上一条数据的水位值
public class Flink28_State_Keyed {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
}
})
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() { // 指定如何从数据中提取事件时间
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L; // 转换成 毫秒
}
})
);
sensorDS
.keyBy(sensor -> sensor.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
//TODO 1 初始化状态
ValueState<Integer> lastVCState;
//如果不用状态保存,而是用变量保存,那么不会分组
int lastVC = 0;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2 初始化状态
lastVCState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("lastVCState", Types.INT, 0));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3 处理数据
// Integer value1 = lastVCState.value();
// out.collect("当前key=" + ctx.getCurrentKey() + ", 上一次的水位值=" + value1);
// lastVCState.update(value.getVc());
//如果不使用状态,使用变量保存:
out.collect("当前key" + ctx.getCurrentKey() + ", 上一次的水位值=" + lastVC);
lastVC = value.getVc();
}
})
.print();
env.execute();
}
}
(5)键控流状态案例2:改造定时器
public class Flink29_State_KeyedState {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
})
.assignTimestampsAndWatermarks(WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
})
)
.keyBy(sensor -> sensor.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
//TODO 1 定义状态
ValueState<Long> timeTs;
ValueState<Integer> lastVC;
//TODO 2 初始化状态
@Override
public void open(Configuration parameters) throws Exception {
timeTs =getRuntimeContext().getState(new ValueStateDescriptor<Long>("timeTs", Types.LONG));
lastVC = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("lastVC", Types.INT, 0));
}
//TODO 3 处理数据
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
if (timeTs.value() == null){
//TODO 3.1 第一条进来,注册一个5s后的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
timeTs.update(ctx.timestamp() + 5000L);
}
if (value.getVc() < lastVC.value()){
//于上一条数据比较水位,判断上升还是下降
//下降
//1. 删除原来的定时器
ctx.timerService().deleteEventTimeTimer(timeTs.value());
//2. 注册新的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
timeTs.update(ctx.timestamp() + 5000L);
}
//保存水位值,给下一次数据做判断
lastVC.update(value.getVc());
}
//TODO 4 定时器触发
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
timeTs.clear();
out.collect("传感器=" + ctx.getCurrentKey() + "在ts=" + timestamp + "监测到水位连续5s上升");
}
})
.print("result");
env.execute();
}
}