Flink中状态编程 完整使用 (第九章)

Flink中状态编程 完整使用

Flink处理机制的核心,就是“有状态的流式计算”。我们在之前的章节中也已经多次提到了“状态”(state),不论是简单聚合、窗口聚合,还是处理函数的应用,都会有状态的身影出现。在第一章中,我们已经简单介绍过有状态流处理,状态就如同事务处理时数据库中保存的信息一样,是用来辅助进行任务计算的数据。而在Flink这样的分布式系统中,我们不仅需要定义出状态在任务并行时的处理方式,还需要考虑如何持久化保存、以便发生故障时正确地恢复。这就需要一套完整的管理机制来处理所有的状态。

一、Flink 中的状态

在流处理中,数据是连续不断到来和处理的。每个任务进行计算处理时,可以基于当前数据直接转换得到输出结果;也可以依赖一些其他数据。这些由一个任务维护,并且用来计算输出结果的所有数据,就叫作这个任务的状态。

1、有状态算子

在Flink中,算子任务可以分为无状态和有状态两种情况。

无状态的算子任务 只需要观察每个独立事件,根据当前输入的数据直接转换输出结果,可以将一个字符串类型的数据拆分开作为元组输出;也可以对数据做一些计算,比如每个代表数量的字段加1。我们之前讲到的基本转换算子,如map、filter、flatMap,计算时不依赖其他数据,就都属于无状态的算子。

在这里插入图片描述

有状态的算子任务 ,则除当前数据之外,还需要一些其他数据来得到计算结果。这里的“其他数据”,就是所谓的状态(state),最常见的就是之前到达的数据,或者由之前数据计算出的某个结果。比如,做求和(sum)计算时,需要保存之前所有数据的和,这就是状态; 窗口算子中会保存已经到达的所有数据,这些也都是它的状态。另外,如果我们希望检索到某种“事件模式”(event pattern),比如“先有下单行为,后有支付行为”,那么也应该把之前的行为保存下来,这同样属于状态。容易发现,之前讲过的聚合算子、窗口算子都属于有状态的算子。

在这里插入图片描述

1)算子任务接收到上游发来的数据;
(2)获取当前状态;
(3)根据业务逻辑进行计算,更新状态;
(4)得到计算结果,输出发送到下游任务。

2、状态的管理

flink本身是一个分布式的处理引擎

在传统的事务型处理架构中,这种额外的状态数据是保存在数据库中的。而对于实时流处理来说,这样做需要频繁读写外部数据库,如果数据规模非常大肯定就达不到性能要求了。所以Flink的解决方案是,将状态直接保存在内存中来保证性能,并通过分布式扩展来提高吞吐量。

在Flink中,每一个算子任务都可以设置并行度,从而可以在不同的slot上并行运行多个实例,我们把它们叫作“并行子任务”。而状态既然在内存中,那么就可以认为是子任务实例上的一个本地变量,能够被任务的业务逻辑访问和修改。

这样看来状态的管理似乎非常简单,我们直接把它作为一个对象交给JVM就可以了。然而大数据的场景下,我们必须使用分布式架构来做扩展,在低延迟、高吞吐的基础上还要保证容错性,一系列复杂的问题就会随之而来了。

状态的访问权限。我们知道Flink上的聚合和窗口操作,一般都是基于KeyedStream的,数据会按照key的哈希值进行分区,聚合处理的结果也应该是只对当前key有效。然而同一个分区(也就是slot)上执行的任务实例,可能会包含多个key的数据,它们同时访问和更改本地变量,就会导致计算结果错误。所以这时状态并不是单纯的本地变量。

容错性,也就是故障后的恢复。状态只保存在内存中显然是不够稳定的,我们需要将它持久化保存,做一个备份;在发生故障后可以从这个备份中恢复状态。

我们还应该考虑到分布式应用的横向扩展性。比如处理的数据量增大时,我们应该相应地对计算资源扩容,调大并行度。这时就涉及到了状态的重组调整。

3、状态的分类

1. 托管状态(Managed State)和原始状态(Raw State)

1、Flink的状态有两种:托管状态(Managed State)和原始状态(Raw State)。托管状态就是由Flink统一管理的,状态的存储访问、故障恢复和重组等一系列问题都由Flink实现,我们只要调接口就可以;而原始状态则是自定义的,相当于就是开辟了一块内存,需要我们自己管理,实现状态的序列化和故障恢复。

2、具体来讲,托管状态是由Flink的运行时(Runtime)来托管的在配置容错机制后,状态会自动持久化保存,并在发生故障时自动恢复当应用发生横向扩展时,状态也会自动地重组分配到所有的子任务实例上。对于具体的状态内容,Flink也提供了值状态(ValueState)、列表状态(ListState)、映射状态(MapState)、聚合状态(AggregateState)等多种结构,内部支持各种数据类型。聚合、窗口等算子中内置的状态,就都是托管状态;我们也可以在富函数类(RichFunction)中通过上下文来自定义状态,这些也都是托管状态。

3、而对比之下,原始状态就全部需要自定义了。Flink不会对状态进行任何自动操作,也不知道状态的具体数据类型,只会把它当作最原始的字节(Byte)数组来存储。我们需要花费大量的精力来处理状态的管理和维护。
所以只有在遇到托管状态无法实现的特殊需求时,我们才会考虑使用原始状态;一般情况下不推荐使用。绝大多数应用场景,我们都可以用Flink提供的算子或者自定义托管状态来实现需求。

2. 算子状态(Operator State)和按键分区状态(Keyed State)

接下来我们的重点就是托管状态(Managed State)。

我们知道在Flink中,一个算子任务会按照并行度分为多个并行子任务执行而不同的子任务会占据不同的任务槽(task slot)。由于不同的slot在计算资源上是物理隔离的,所以Flink能管理的状态在并行任务间是无法共享的,每个状态只能针对当前子任务的实例有效

而很多有状态的操作(比如聚合、窗口)都是要先做keyBy进行按键分区的。按键分区之后,任务所进行的所有计算都应该只针对当前key有效,所以状态也应该按照key彼此隔离。在这种情况下,状态的访问方式又会有所不同。

基于这样的想法,我们又可以将托管状态分为两类:算子状态和按键分区状态。

(1)算子状态(Operator State)

状态作用范围限定为当前的算子任务实例,也就是只对当前并行子任务实例有效。这就意味着对于一个并行子任务,占据了一个“分区”,它所处理的所有数据都会访问到相同的状态,状态对于同一任务而言是共享的,如图9-3所示。

在这里插入图片描述

算子状态可以用在所有算子上,使用的时候其实就跟一个本地变量没什么区别——因为本地变量的作用域也是当前任务实例。在使用时,我们还需进一步实现CheckpointedFunction接口。

(2)按键分区状态(Keyed State)

状态是根据输入流中定义的键(key)来维护和访问的,所以只能定义在按键分区流(KeyedStream)中也就keyBy之后才可以使用,如图9-4所示。

在这里插入图片描述

按键分区状态应用非常广泛。之前讲到的聚合算子必须在keyBy之后才能使用,就是因为聚合的结果是以Keyed State的形式保存的。另外,也可以通过富函数类(Rich Function)来自定义Keyed State所以只要提供了富函数类接口的算子也都可以使用Keyed State。

所以即使是map、filter这样无状态的基本转换算子,我们也可以通过富函数类给它们“追加”Keyed State,或者实现CheckpointedFunction接口来定义Operator State;从这个角度讲,Flink中所有的算子都可以是有状态的,不愧是“有状态的流处理”。

无论是Keyed State还是Operator State,它们都是在本地实例上维护的,也就是说每个并行子任务维护着对应的状态,算子的子任务之间状态不共享。关于状态的具体使用,我们会在下面继续展开讲解。

二、按键分区状态(Keyed State)

在实际应用中,我们一般都需要将数据按照某个key进行分区,然后再进行计算处理;所以最为常见的状态类型就是Keyed State。之前介绍到keyBy之后的聚合、窗口计算,算子所持有的状态,都是Keyed State

另外,我们还可以通过富函数类(Rich Function)对转换算子进行扩展、实现自定义功能,比如RichMapFunction、RichFilterFunction。在富函数中,我们可以调用.getRuntimeContext()获取当前的运行时上下文(RuntimeContext),进而获取到访问状态的句柄;这种富函数中自定义的状态也是Keyed State

1、基本概念和特点

按键分区状态(Keyed State)顾名思义是任务按照键(key)来访问和维护的状态。它的特点非常鲜明,就是以key为作用范围进行隔离。

我们知道,在进行按键分区(keyBy)之后,具有相同键的所有数据,都会分配到同一个并行子任务中;所以如果当前任务定义了状态,Flink就会在当前并行子任务实例中,为每个键值维护一个状态的实例。于是当前任务就会为分配来的所有数据,按照key维护和处理对应的状态。

因为一个并行子任务可能会处理多个key的数据,所以Flink需要对Keyed State进行一些特殊优化。在底层,Keyed State类似于一个分布式的映射(map)数据结构,所有的状态会根据key保存成键值对(key-value)的形式。这样当一条数据到来时,任务就会自动将状态的访问范围限定为当前数据的key,从map存储中读取出对应的状态值。所以具有相同key的所有数据都会到访问相同的状态,而不同key的状态之间是彼此隔离的。

这种将状态绑定到key上的方式,相当于使得状态和流的逻辑分区一一对应了:不会有别的key的数据来访问当前状态;而当前状态对应key的数据也只会访问这一个状态,不会分发到其他分区去。这就保证了对状态的操作都是本地进行的,对数据流和状态的处理做到了分区一致性。

另外,在应用的并行度改变时,状态也需要随之进行重组不同key对应的Keyed State可以进一步组成所谓的键组(key groups),每一组都对应着一个并行子任务。键组是Flink重新分配Keyed State的单元,键组的数量就等于定义的最大并行度。当算子并行度发生改变时,Keyed State就会按照当前的并行度重新平均分配,保证运行时各个子任务的负载相同。

需要注意,使用Keyed State必须基于KeyedStream。没有进行keyBy分区的DataStream,即使转换算子实现了对应的富函数类,也不能通过运行时上下文访问Keyed State。

2、支持的结构类型

实际应用中,需要保存为状态的数据会有各种各样的类型,有时还需要复杂的集合类型,比如列表(List)和映射(Map)。对于这些常见的用法,Flink的按键分区状态(Keyed State)提供了足够的支持。接下来我们就来了解一下Keyed State 所支持的结构类型.

1. 值状态(ValueState)

顾名思义,状态中只保存一个“值”(value)。ValueState本身是一个接口,源码中定义如下:

public interface ValueState<T> extends State {
   T value() throws IOException;
   void update(T value) throws IOException;
}

这里的T是泛型,表示状态的数据内容可以是任何具体的数据类型。如果想要保存一个长整型值作为状态,那么类型就是ValueState<Long>。
我们可以在代码中读写值状态,实现对于状态的访问和更新。
T value():获取当前状态的值;
update(T value):对状态进行更新,传入的参数value就是要覆写的状态值

在具体使用时,为了让运行时上下文清楚到底是哪个状态,我们还需要创建一个“状态描述器”(StateDescriptor)提供状态的基本信息。例如源码中,ValueState的状态描述器构造方法如下:

public ValueStateDescriptor(String name, Class<T> typeClass) {
    super(name, typeClass, null);
}
1.1 普通代码
```java
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * 按键分区状态
 * 基本方式和值状态
 * keyedState
 */

public class StateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user)
                .flatMap(new MyFlatMap())
                .print();
        env.execute();

    }


    //TODO 实现自定义的·FlatMapFunction,用于keyed,Stated 测试
    // IN-输入, OUT-输出
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        /**
         *
         *  定义属性、直接以赋初值的状态、去调用getRuntimeContext()、其实是无效的
         *  在这个时候当前其实是静态的代码、本身当前在执行这段代码d额时候、当前这个类的实例是没有创建出来的、
         *  --------所以直接调用当前getRuntimeContext()上下文、其实是不对的、怎么去处理这个问题?
         *          -----直接放到open声明周期里、open周期调用的时候当然是当前的运行时环境都就就位了
         *
         */

        //ValueState<Event> myValueState = getRuntimeContext().getState(new ValueStateDescriptor<Event>("my-state", Event.class));

        //定义状态
        ValueState<Event> myValueState;

        /**
         * 放在open方法里面、初始化的时候获取状态
         *
         * @param parameters 状态名称
         * @param parameters 状态类型
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
            myValueState = getRuntimeContext().getState(new ValueStateDescriptor<Event>("my-state", Event.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
            System.out.println(myValueState.value());

            /**
             * 把当前的value写入到当前的状态里面
             * 没来一个数据就更新下状态
             */
            myValueState.update(value);
            System.out.println("my value: " + myValueState.value());
        }
    }
}


1.2 示例代码统计
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

import java.time.Duration;


/**
 * 按键分区
 * 应用实例
 * 值状态
 *
 * 我们这里会使用用户id来进行分流,然后分别统计每个用户的pv数据,由于我们并不想每次pv加一,就将统计结果发送到下游去,所以这里我们注册了一个定时器,
 * 用来隔一段时间发送pv的统计结果,这样对下游算子的压力不至于太大。具体实现方式是定义一个用来保存定时器时间戳的值状态变量。当定时器触发并向下游发送数据以后,
 * 便清空储存定时器时间戳的状态变量,  这样当新的数据到来时,发现并没有定时器存在,就可以注册新的定时器了,注册完定时器之后将定时器的时间戳继续保存在状态变量中。
 */
public class PeriodicPvExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);


        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        })
                );


            stream.print("input");

        //统计每个用户的PV
        stream.keyBy(data -> data.user)
                .process(new PeriodicPvResult())
                .print();

        env.execute();

    }

    /**
     * String key 的类型
     * Event 输入
     * String 输出
     */
    public static class PeriodicPvResult extends KeyedProcessFunction<String, Event, String> {


        //定义状态,保存当前PV统计值、以及有没有定时器
        ValueState<Long> countState;

        //定义定时器
        ValueState<Long> timerTsState;

        @Override
        public void open(Configuration parameters) throws Exception {
            //"count"   名称 , Long.class 类型
            countState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("count", Long.class));
            timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("timer-ts", Long.class));
        }

        /**
         * 每条数据到来的时候、都会调用它
         * @param value
         * @param ctx
         * @param out
         * @throws Exception
         */
        @Override
        public void processElement(Event value, KeyedProcessFunction<String, Event, String>.Context ctx, Collector<String> out) throws Exception {
            //每来一条数据,就更新对应的count值
            Long count = countState.value();

            //判断count 里面什么都没有的话、那就更新1、如果不是空的话 就count+1

            countState.update(count == null ? 1 : count + 1);

            //如果没有注册过的话, --- 注册定时器
            if (timerTsState.value() == null) {
                ctx.timerService().registerEventTimeTimer(value.timestamp + 10 * 1000);
                timerTsState.update(value.timestamp + 10 * 1000);
            }
        }

        @Override
        public void onTimer(long timestamp, KeyedProcessFunction<String, Event, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
            //定时器触发,输出一次统计结果  当前key的用户有了 ctx.getCurrentKey()
            out.collect(ctx.getCurrentKey() + "pv: " + countState.value());

            //清空状态
            timerTsState.clear();
            ctx.timerService().registerEventTimeTimer(timestamp + 10 * 1000);
            timerTsState.update(timestamp + 10 * 1000);
        }
    }
}

这里需要传入状态的名称和类型——这跟我们声明一个变量时做的事情完全一样有了这个描述器,运行时环境就可以获取到状态的控制句柄(handler)了。关于代码中状态的使用,我们会在下一节详细介绍。

2. 列表状态(ListState)

将需要保存的数据,以列表(List)的形式组织起来。在ListState<T>接口中同样有一个类型参数T,表示列表中数据的类型。ListState也提供了一系列的方法来操作状态,使用方式与一般的List非常相似。

Iterable<T> get():获取当前的列表状态,返回的是一个可迭代类型Iterable<T>;
⚪ update(List<T> values):传入一个列表values,直接对状态进行覆盖;
⚪ add(T value):在状态列表中添加一个元素value;
⚪ addAll(List<T> values):向列表中添加多个元素,以列表values形式传入。

类似地,ListState的状态描述器就叫作ListStateDescriptor,用法跟ValueStateDescriptor完全一致。

2.1 代码
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * ListStateTest
 * 状态编程
 * 案件分区
 * 代码中的使用
 * 基本方式和值状态
 */
public class ListStateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //水位线 ----使用乱序流中的方法
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                //时间戳
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user) //user作为key去分组
                //TODO 定义一个算子、在这个算子里面定义一个状态 ------flatMap 无状态的
                // 所有的算子都可以使用他的富含数类
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的flatMapFunction,用keyed State 测试
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        //定义状态
        ListState<Event> myListState;

        //增加一个本地变量进行对比
        Long count = 0L;

        /**
         * 为了 全局使用这个值
         * 在生命周期中创建
         *
         * @param parameters
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
            myListState = getRuntimeContext().getListState(new ListStateDescriptor<Event>("my-list", Event.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
    
            myListState.add(value);
            System.out.println("myListState: " + myListState.get());
       
        }
    }

}

2.2 实例代码全外链接合并
package com.example.chapter09;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.CoProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 按键分区状态
 * 应用实例
 * 列表状态 ---全外连接
 * 在Flink SQL中,支持两条流的全量Join,语法如下:
 * SELECT * FROM A INNER JOIN B WHERE A.id = B.id;
 * 这样一条SQL语句要慎用,因为Flink会将A流和B流的所有数据都保存下来,然后进行Join。不过在这里我们可以用列表状态变量来实现一下这个SQL语句的功能
 */
public class TwoStreamJoinExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Tuple3<String, String, Long>> stream = env.fromElements(
                Tuple3.of("a", "stream-1", 1000L),
                Tuple3.of("b", "stream-1", 2000L),
                Tuple3.of("a", "stream-1", 3000L)
                //forMonotonousTimestamps 升序单调递增生成对应的Watermark
        ).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String, String, Long>>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String, String, Long>>() {
                    @Override
                    public long extractTimestamp(Tuple3<String, String, Long> element, long recordTimestamp) {
                        return element.f2;
                    }
                }));

        SingleOutputStreamOperator<Tuple3<String, String, Long>> stream2 = env.fromElements(
                Tuple3.of("a", "stream-2", 3000L),
                Tuple3.of("b", "stream-2", 4000L),
                Tuple3.of("a", "stream-2", 5000L)
        ).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String, String, Long>>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String, String, Long>>() {
                    @Override
                    public long extractTimestamp(Tuple3<String, String, Long> element, long recordTimestamp) {
                        return element.f2;
                    }
                }));


        //自定义列表状态进行全外连接
        stream.keyBy(data -> data.f0)
                .connect(stream2.keyBy(data -> data.f0))
                .process(new CoProcessFunction<Tuple3<String, String, Long>, Tuple3<String, String, Long>, String>() {

                    //定义列表状态,用于保存两条流中已经到达的所有数据
                    private ListState<Tuple2<String, Long>> stream1ListState;
                    private ListState<Tuple2<String, Long>> stream2ListState;

                    /**
                     * 在声明周期中创建
                     * @param parameters
                     * @throws Exception
                     */
                    @Override
                    public void open(Configuration parameters) throws Exception {
                        stream1ListState = getRuntimeContext().getListState(new ListStateDescriptor<Tuple2<String, Long>>("stream-list", Types.TUPLE(Types.STRING, Types.LONG)));
                        stream2ListState = getRuntimeContext().getListState(new ListStateDescriptor<Tuple2<String, Long>>("stream2-list", Types.TUPLE(Types.STRING, Types.LONG)));
                    }

                    @Override
                    public void processElement1(Tuple3<String, String, Long> left, Context ctx, Collector<String> out) throws Exception {

                        //获取另一条流中的所有数据,配对输出
                        for (Tuple2<String, Long> right : stream2ListState.get()) {
                            out.collect(left.f0 + " " + left.f2 + " => " + right);
                        }

                        //添加到第一条流当中的
                        stream1ListState.add(Tuple2.of(left.f0, left.f2));
                    }

                    @Override
                    public void processElement2(Tuple3<String, String, Long> right, Context ctx, Collector<String> out) throws Exception {
                        //获取另一条流中的所有数据,配对输出
                        for (Tuple2<String, Long> left : stream1ListState.get()) {
                            out.collect(left + " => " + right.f0 + " " + right.f2);
                        }

                        stream2ListState.add(Tuple2.of(right.f0, right.f2));

                    }


                })
                .print();
        env.execute();


    }
}

3. 映射状态(MapState)

把一些键值对(key-value)作为状态整体保存起来,可以认为就是一组key-value映射的列表。对应的MapState<UK, UV>接口中,就会有UK、UV两个泛型,分别表示保存的key和value的类型。同样,MapState提供了操作映射状态的方法,与Map的使用非常类似。

UV get(UK key):传入一个key作为参数,查询对应的value值;
⚪ put(UK key, UV value):传入一个键值对,更新key对应的value值;
⚪ putAll(Map<UK, UV> map):将传入的映射map中所有的键值对,全部添加到映射状态中;
⚪ remove(UK key):将指定key对应的键值对删除;
⚪ boolean contains(UK key):判断是否存在指定的key,返回一个boolean值。
另外,MapState也提供了获取整个映射相关信息的方法:
⚪ Iterable<Map.Entry<UK, UV>> entries():获取映射状态中所有的键值对;
⚪ Iterable<UK> keys():获取映射状态中所有的键(key),返回一个可迭代Iterable类型;
⚪ Iterable<UV> values():获取映射状态中所有的值(value),返回一个可迭代Iterable类型;
boolean isEmpty():判断映射是否为空,返回一个boolean
3.1 代码
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * ListStateTest
 * 状态编程
 * 案件分区
 * 代码中的使用
 * 基本方式和值状态
 */
public class ListStateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //水位线 ----使用乱序流中的方法
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                //时间戳
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user) //user作为key去分组
                //TODO 定义一个算子、在这个算子里面定义一个状态 ------flatMap 无状态的
                // 所有的算子都可以使用他的富含数类
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的flatMapFunction,用keyed State 测试
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        //定义状态
        MapState<String, Long> myMapState;


        /**
         * 为了 全局使用这个值
         * 在生命周期中创建
         *
         * @param parameters
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
            myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("my-map", String.class, Long.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
            myMapState.put(value.user, myMapState.get(value.user) == null ? 1 : myMapState.get(value.user) + 1);
            System.out.println("my map state :" + value.user + " " + myMapState.get(value.user));
        }
    }

}

3.2 示例代码 模拟窗口功能
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

import java.sql.Timestamp;
import java.time.Duration;

/**
 * 按键分区状态
 * 应用实例
 * 映射状态
 * TODO 我们模拟一个滚动窗口。我们要计算的是每一个url在每一个窗口中的pv数据
 *  映射状态的用法和Java中的HashMap很相似。在这里我们可以通过MapState的使用来探索一下窗口的底层实现,也就是我们要用映射状态来完整模拟窗口的功能。
 *  这里我们模拟一个滚动窗口。 我们要计算的是每一个url在每一个窗口中的pv数据。我们之前使用增量聚合和全窗口聚合结合的方式实现过这个需求。
 *  这里我们用MapState再来实现一下。
 */
public class FakeWindowExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.getConfig().setAutoWatermarkInterval(100);


        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //乱序流watermark 生成
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        })
                );
        stream.print("input");

        //用到了状态和定时器、要使用process
        stream.keyBy(data -> data.url)
                .process(new FakeWindowResult(10000L))
                .print();

        env.execute();
    }

    //实现自定义的keyedProcessFunction
    public static class FakeWindowResult extends KeyedProcessFunction<String, Event, String> {

        private Long windowSize; //窗口大小

        public FakeWindowResult(Long windowSize) {
            this.windowSize = windowSize;
        }

        //TODO 定义一个MapState,用来保存每个窗口中统计的count值
        MapState<Long, Long> windowUrlCountMapState;

        @Override
        public void open(Configuration parameters) throws Exception {
            windowUrlCountMapState = getRuntimeContext().getMapState(new MapStateDescriptor<Long, Long>("window-count", Long.class, Long.class));
        }

        @Override
        public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {

            //每来一条数据,根据时间戳判断属于那个窗口 (窗口分配器) 整10秒的起始点
            Long windowStart = value.timestamp / windowSize * windowSize;

            Long windowEnd = windowStart + windowSize;

            //注册end -1 的定时器
            ctx.timerService().registerEventTimeTimer(windowEnd - 1);

            //更新状态,进行增量聚合
            if (windowUrlCountMapState.contains(windowStart)) {
                Long count = windowUrlCountMapState.get(windowStart);
                windowUrlCountMapState.put(windowStart, count + 1);
            } else
                windowUrlCountMapState.put(windowStart, 1L);
        }

        //定时器触发时输出结果
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            //窗口结束时间
            long windowEnd = timestamp + 1;
            //窗口开始时间
            long windowStart = windowEnd - windowSize;
            Long count = windowUrlCountMapState.get(windowStart);

            out.collect("窗口" + new Timestamp(windowStart) + " ~ " + new Timestamp(windowEnd)
                    + " url: " + ctx.getCurrentKey()
                    + " count: " + count);

            //模拟窗口的关闭,清除map中对应的key-value
            windowUrlCountMapState.remove(windowStart);
        }
    }
}

4. 归约状态(ReducingState)

类似于值状态(Value),不过需要对添加进来的所有数据进行归约,将归约聚合之后的值作为状态保存下来。ReducintState这个接口调用的方法类似于ListState,只不过它保存的只是一个聚合值,所以调用.add()方法时,不是在状态列表里添加元素,而是直接把新数据和之前的状态进行归约,并用得到的结果更新状态。

归约逻辑的定义,是在归约状态描述器(ReducingStateDescriptor)中,通过传入一个归约函数(ReduceFunction)来实现的。这里的归约函数,就是我们之前介绍reduce聚合算子时讲到的ReduceFunction,所以状态类型跟输入的数据类型是一样的。

public ReducingStateDescriptor(
        String name, ReduceFunction<T> reduceFunction, Class<T> typeClass) {...}

这里的描述器有三个参数,其中第二个参数就是定义了归约聚合逻辑的ReduceFunction,另外两个参数则是状态的名称和类型。

4.1 代码
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * ListStateTest
 * 状态编程
 * 案件分区
 * 代码中的使用
 * 基本方式和值状态
 */
public class ListStateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //水位线 ----使用乱序流中的方法
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                //时间戳
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user) //user作为key去分组
                //TODO 定义一个算子、在这个算子里面定义一个状态 ------flatMap 无状态的
                // 所有的算子都可以使用他的富含数类
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的flatMapFunction,用keyed State 测试
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        //定义状态
        ReducingState<Event> myReducingState;

        //增加一个本地变量进行对比
        Long count = 0L;

        /**
         * 为了 全局使用这个值
         * 在生命周期中创建
         *
         * @param parameters
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
      
            myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Event>("my-redu",
                    new ReduceFunction<Event>() {
                        @Override
                        public Event reduce(Event value1, Event value2) throws Exception {
                            return new Event(value1.user, value1.url, value2.timestamp);
                        }
                    }
                    , Event.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
  
          myReducingState.add(value);
            System.out.println("reducing state: " + myReducingState.get());
            count++;
        }
    }

}

5. 聚合状态(AggregatingState)

与归约状态非常类似,聚合状态也是一个值,用来保存添加进来的所有数据的聚合结果。与ReducingState不同的是,它的聚合逻辑是由在描述器中传入一个更加一般化的聚合函数

(AggregateFunction)来定义的;这也就是之前我们讲过的AggregateFunction,里面通过一个累加器(Accumulator)来表示状态,所以聚合的状态类型可以跟添加进来的数据类型完全不同,使用更加灵活。

同样地,AggregatingState接口调用方法也与ReducingState相同,调用.add()方法添加元素时,会直接使用指定的AggregateFunction进行聚合并更新状态。

5.1 代码
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * ListStateTest
 * 状态编程
 * 案件分区
 * 代码中的使用
 * 基本方式和值状态
 */
public class ListStateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //水位线 ----使用乱序流中的方法
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                //时间戳
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user) //user作为key去分组
                //TODO 定义一个算子、在这个算子里面定义一个状态 ------flatMap 无状态的
                // 所有的算子都可以使用他的富含数类
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的flatMapFunction,用keyed State 测试
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        //定义状态
        AggregatingState<Event, String> myAggregatingState;


        /**
         * 为了 全局使用这个值
         * 在生命周期中创建
         *
         * @param parameters
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
            myAggregatingState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Long, String>("my-agg",
                    new AggregateFunction<Event, Long, String>() {

                        @Override
                        public Long createAccumulator() {
                            return 0L;
                        }

                        @Override
                        public Long add(Event value, Long accumulator) {
                            return accumulator + 1;
                        }

                        @Override
                        public String getResult(Long accumulator) {
                            return "count: " + accumulator;
                        }

                        @Override
                        public Long merge(Long a, Long b) {
                            return a + b;
                        }
                    }
                    , Long.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
            myAggregatingState.add(value);
            System.out.println("my agg state: " + myAggregatingState.get());

        }
    }

}

5.2 示例代码用户每点击5次的平均时间戳
package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.AggregatingState;
import org.apache.flink.api.common.state.AggregatingStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * 按键分区
 * 应用实例
 * 聚合状态
 *
 * 我们举一个简单的例子,对用户点击事件流每5个数据统计一次平均时间戳。
 * 这是一个类似计数窗口(CountWindow)求平均值的计算,这里我们可以使用一个有聚合状态的RichFlatMapFunction来实现。
 */
public class AverageTimestampExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        })
                );

        stream.print("input");

        //自定义实现平均时间戳统计
        stream.keyBy(data -> data.user)
                .flatMap(new AvgResult(5L))
                .print();
        env.execute();
    }

    public static class AvgResult extends RichFlatMapFunction<Event, String> {

        //计数、达到这个值的时候就要输出结果
        private Long count;

        public AvgResult(Long count) {
            this.count = count;
        }

        //定义聚合的状态、用来保存平均的时间戳 --- 当前拿到所有时间戳的总和
        AggregatingState<Event, Long> avgTsAggState;

        //定义一个值状态、保存用户访问次数 ---当前次数
        ValueState<Long> countState;

        //Tuple2<Long,Long> 存储 个数、总和
        @Override
        public void open(Configuration parameters) throws Exception {
            avgTsAggState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Tuple2<Long, Long>, Long>(
                    "avg-ts",
                    new AggregateFunction<Event, Tuple2<Long, Long>, Long>() {
                        @Override
                        public Tuple2<Long, Long> createAccumulator() {
                            return Tuple2.of(0L, 0L);
                        }

                        /**
                         * 之前 accumulator基础上
                         *      第一个位置当成sum的话 accumulator.f0 + value.timestamp、所有数据时间戳的和
                         *      第二个位置当成count的话 accumulator.f1 + 1
                         * @param value
                         * @param accumulator
                         * @return
                         */
                        @Override
                        public Tuple2<Long, Long> add(Event value, Tuple2<Long, Long> accumulator) {
                            return Tuple2.of(accumulator.f0 + value.timestamp, accumulator.f1 + 1);
                        }

                        @Override
                        public Long getResult(Tuple2<Long, Long> accumulator) {
                            return accumulator.f0 / accumulator.f1;
                        }

                        @Override
                        public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
                            return null;
                        }
                    }
                    ,
                    Types.TUPLE(Types.LONG, Types.LONG)
            ));
            countState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("count", Long.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
            //每来一条数据 curr count 加1
            Long currCount = countState.value();
            if (currCount == null) {
                currCount = 1L;
            } else {
                currCount++;
            }

            //更新状态 直接改当前的count值
            countState.update(currCount);

            //存储整合
            avgTsAggState.add(value);

            //如果达到count次数就输出结果
            if (currCount.equals(count)) {
                out.collect(value.user + "" + " 过去" + count + "次平均访问时间戳为: " + avgTsAggState.get());

                //清理状态
                countState.clear();

                //想实现每隔五次我就要输出统计一次当前的所有访问的平均时间戳
                //只把当前的 avgTsAggState清除

//                avgTsAggState.clear();
            }

        }
    }
}

3、代码实现

1. 整体介绍

在 Flink 中,状态始终是与特定算子相关联的;算子在使用状态前首先需要“注册”,其实就是告诉Flink当前上下文中定义状态的信息,这样运行时的 Flink 才能知道算子有哪些状态。

状态的注册,主要是通过“状态描述器”(StateDescriptor)来实现的。状态描述器中最重要的内容,就是状态的名称(name)和类型(type)。我们知道Flink中的状态,可以认为是加了一些复杂操作的内存中的变量;而当我们在代码中声明一个局部变量时,都需要指定变量类型和名称,名称就代表了变量在内存中的地址,类型则指定了占据内存空间的大小。同样地,我们一旦指定了名称和类型,Flink就可以在运行时准确地在内存中找到对应的状态,进而返回状态对象供我们使用了。所以在一个算子中,我们也可以定义多个状态,只要它们的名称不同就可以了。

另外,状态描述器中还可能需要传入一个用户自定义函数(user-defined-function,UDF),用来说明处理逻辑,比如前面提到的ReduceFunction和AggregateFunction。

以ValueState为例,我们可以定义值状态描述器如下:

ValueStateDescriptor<Long> descriptor = new ValueStateDescriptor<>(
"my state", // 状态名称
Types.LONG // 状态类型
);

这里我们定义了一个叫作“my state”的长整型ValueState的描述器。

代码中完整的操作是,首先定义出状态描述器;然后调用.getRuntimeContext()方法获取运行时上下文;继而调用RuntimeContext的获取状态的方法,将状态描述器传入,就可以得到对应的状态了。

因为状态的访问需要获取运行时上下文,这只能在富函数类(Rich Function)中获取到,所以自定义的Keyed State只能在富函数中使用。当然,底层的处理函数(Process Function)本身继承了AbstractRichFunction抽象类,所以也可以使用。

在富函数中,调用.getRuntimeContext()方法获取到运行时上下文之后,RuntimeContext有以下几个获取状态的方法:

ValueState<T> getState(ValueStateDescriptor<T>)
MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)
ListState<T> getListState(ListStateDescriptor<T>)
ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)

对于不同结构类型的状态,只要传入对应的描述器、调用对应的方法就可以了。

获取到状态对象之后,就可以调用它们各自的方法进行读写操作了。另外,所有类型的状态都有一个方法.clear(),用于清除当前状态。

2、keyedState 代码

因为RichFlatmapFunction中的.flatmap()是每来一条数据都会调用一次的,所以我们不应该在这里调用运行时上下文的.getState()方法,而是在生命周期方法.open()中获取状态对象。另外还有一个问题,我们获取到的状态对象也需要有一个变量名称state(注意这里跟状态的名称my state不同),但这个变量不应该在open中声明——否则在.flatmap()里就访问不到了。所以我们还需要在外面直接把它定义为类的属性,这样就可以在不同的方法中通用了。而在外部又不能直接获取状态,因为编译时是无法拿到运行时上下文的。所以最终的解决方案就变成了:在外部声明状态对象,在open生命周期方法中通过运行时上下文获取状态。

这里需要注意,这种方式定义的都是Keyed State,它对于每个key都会保存一份状态实例。所以对状态进行读写操作时,获取到的状态跟当前输入数据的key有关;只有相同key的数据,才会操作同一个状态,不同key的数据访问到的状态值是不同的。而且上面提到的.clear()方法,也只会清除当前key对应的状态。

另外,状态不一定都存储在内存中,也可以放在磁盘或其他地方,具体的位置是由一个可配置的组件来管理的,这个组件叫作“状态后端”(State Backend)。关于状态后端

4、状态生存时间(TTL)

在实际应用中,很多状态会随着时间的推移逐渐增长,如果不加以限制,最终就会导致存储空间的耗尽。一个优化的思路是直接在代码中调用.clear()方法去清除状态,但是有时候我们的逻辑要求不能直接清除。这时就需要配置一个状态的“生存时间”(time-to-live,TTL),当状态在内存中存在的时间超出这个值时,就将它清除。

具体实现上,如果用一个进程不停地扫描所有状态看是否过期,显然会占用大量资源做无用功。状态的失效其实不需要立即删除,所以我们可以给状态附加一个属性,也就是状态的“失效时间”。状态创建的时候,设置 失效时间 = 当前时间 + TTL;之后如果有对状态的访问和修改,我们可以再对失效时间进行更新;当设置的清除条件被触发时(比如,状态被访问的时候,或者每隔一段时间扫描一次失效状态),就可以判断状态是否失效、从而进行清除了。

配置状态的TTL时,需要创建一个StateTtlConfig配置对象,然后调用状态描述器的.enableTimeToLive()方法启动TTL功能。

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(10))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("my state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

这里用到了几个配置项:

.newBuilder()
状态TTL配置的构造器方法,必须调用,返回一个Builder之后再调用.build()方法就可以得到StateTtlConfig了。方法需要传入一个Time作为参数,这就是设定的状态生存时间。

.setUpdateType()
设置更新类型。更新类型指定了什么时候更新状态失效时间,这里的OnCreateAndWrite表示只有创建状态和更改状态(写操作)时更新失效时间。另一种类型OnReadAndWrite则表示无论读写操作都会更新失效时间,也就是只要对状态进行了访问,就表明它是活跃的,从而延长生存时间。这个配置默认为OnCreateAndWrite。

.setStateVisibility()
设置状态的可见性。所谓的“状态可见性”,是指因为清除操作并不是实时的,所以当状态过期之后还有可能基于存在,这时如果对它进行访问,能否正常读取到就是一个问题了。这里设置的NeverReturnExpired是默认行为,表示从不返回过期值,也就是只要过期就认为它已经被清除了,应用不能继续读取;这在处理会话或者隐私数据时比较重要。对应的另一种配置是ReturnExpireDefNotCleanedUp,就是如果过期状态还存在,就返回它的值。

除此之外,TTL配置还可以设置在保存检查点(checkpoint)时触发清除操作,或者配置增量的清理(incremental cleanup),还可以针对RocksDB状态后端使用压缩过滤器(compaction filter)进行后台清理。关于检查点和状态后端的内容,我们会在后续章节继续讲解。

这里需要注意,目前的TTL设置只支持处理时间。另外,所有集合类型的状态(例如ListState、MapState)在设置TTL时,都是针对每一项(per-entry)元素的。也就是说,一个列表状态中的每一个元素,都会以自己的失效时间来进行清理,而不是整个列表一起清理。

4.1 代码

package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;


/**
 * 按键分区状态
 * 状态生存时间
 */
public class TTLConfigState {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //水位线 ----使用乱序流中的方法
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                //时间戳
                                return element.timestamp;
                            }
                        })
                );

        stream.keyBy(data -> data.user) //user作为key去分组
                //定义一个算子、在这个算子里面定义一个状态 ------flatMap 无状态的
                .flatMap(new ListStateTest.MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的flatMapFunction,用keyed State 测试
    public static class MyFlatMap extends RichFlatMapFunction<Event, String> {

        //定义状态
        ValueState<Event> myValueState;

        //定义状态
        ListState<Event> myListState;

        //定义状态
        MapState<String, Long> myMapState;

        //定义状态
        ReducingState<Event> myReducingState;

        //定义状态
        AggregatingState<Event, String> myAggregatingState;

        //增加一个本地变量进行对比
        Long count = 0L;

        /**
         * 为了 全局使用这个值
         * 在生命周期中创建
         *
         * @param parameters
         * @throws Exception
         */
        @Override
        public void open(Configuration parameters) throws Exception {
            // TODO 单独抽出来
            ValueStateDescriptor<Event> eventValueStateDescriptor = new ValueStateDescriptor<>("my-state", Event.class);
            myValueState = getRuntimeContext().getState(eventValueStateDescriptor);

            myListState = getRuntimeContext().getListState(new ListStateDescriptor<Event>("my-list", Event.class));
            myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("my-map", String.class, Long.class));
            myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Event>("my-redu",
                    new ReduceFunction<Event>() {
                        @Override
                        public Event reduce(Event value1, Event value2) throws Exception {
                            return new Event(value1.user, value1.url, value2.timestamp);
                        }
                    }
                    , Event.class));

            myAggregatingState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Long, String>("my-agg",
                    new AggregateFunction<Event, Long, String>() {

                        @Override
                        public Long createAccumulator() {
                            return 0L;
                        }

                        @Override
                        public Long add(Event value, Long accumulator) {
                            return accumulator + 1;
                        }

                        @Override
                        public String getResult(Long accumulator) {
                            return "count: " + accumulator;
                        }

                        @Override
                        public Long merge(Long a, Long b) {
                            return a + b;
                        }
                    }, Long.class));

            // 配置状态测TTL --- (Time.hours(1)) 一小时失效 TODO 系统时间 机器时间过一小时后
            //如果这个状态超过一小时没有更新、接下来就清除掉
            StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.hours(1))
                    //只有创建和更改才会去修改
                    //   Disabled,
                    //    OnCreateAndWrite,创建和更改 读写 操作的时候才去更新当前的 失效时间
                    //     OnReadAndWrite  无论读写操作都去更新 当前的 失效时间
                    .setUpdateType(StateTtlConfig.UpdateType.OnReadAndWrite)

                    //   ReturnExpiredIfNotCleanedUp, 只要没有删除、就返回
                    //    NeverReturnExpired 永远不要返回失效的数据
                    .setStateVisibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
                    .build();

            //当前的描述器就有了对应的失效的时间的配置和属性
            eventValueStateDescriptor.enableTimeToLive(ttlConfig);

        }

        @Override
        public void flatMap(Event value, Collector<String> out) throws Exception {
            //访问和更新
            System.out.println("第一次 myValueState: " + myValueState.value());
            myValueState.update(value);

           //System.out.println("my value: " + myValueState.value());
            myListState.add(value);

            myMapState.put(value.user, myMapState.get(value.user) == null ? 1 : myMapState.get(value.user) + 1);
            System.out.println("my map state :" + value.user + " " + myMapState.get(value.user));

            myAggregatingState.add(value);
            System.out.println("my agg state: " + myAggregatingState.get());

            myReducingState.add(value);
            System.out.println("reducing state: " + myReducingState.get());
            count++;

            System.out.println("count: " + count);
        }
    }

}

三、算子状态(Operator State)

除按键分区状态(Keyed State)之外,另一大类受控状态就是算子状态(Operator State)。从某种意义上说,算子状态是更底层的状态类型,因为它只针对当前算子并行任务有效,不需要考虑不同key的隔离。算子状态功能不如按键分区状态丰富,应用场景较少,它的调用方法也会有一些区别。

1、基本概念和特点

算子状态(Operator State)就是一个算子并行实例上定义的状态,作用范围被限定为当前算子任务。算子状态跟数据的key无关,所以不同key的数据只要被分发到同一个并行子任务,就会访问到同一个Operator State。

算子状态的实际应用场景不如Keyed State多一般用在Source或Sink等与外部系统连接的算子上,或者完全没有key定义的场景。比如Flink的Kafka连接器中,就用到了算子状态。在我们给Source算子设置并行度后,Kafka消费者的每一个并行实例,都会为对应的主题(topic)分区维护一个偏移量, 作为算子状态保存起来。这在保证Flink应用“精确一次”(exactly-once)状态一致性时非常有用。关于状态一致性的内容,我们会在第十章详细展开。

当算子的并行度发生变化时,算子状态也支持在并行的算子任务实例之间做重组分配。根据状态的类型不同,重组分配的方案也会不同。

2、状态类型

算子状态也支持不同的结构类型,主要有三种:ListState、UnionListState和BroadcastState。

算子状态也支持不同的结构类型,主要有三种:ListState、UnionListState和BroadcastState。

1. 列表状态(ListState)

与Keyed State中的ListState一样,将状态表示为一组数据的列表。

与Keyed State中的列表状态的区别是:在算子状态的上下文中,不会按键(key)分别处理状态,所以每一个并行子任务上只会保留一个“列表”(list),也就是当前并行子任务上所有状态项的集合。列表中的状态项就是可以重新分配的最细粒度,彼此之间完全独立。

当算子并行度进行缩放调整时,算子的列表状态中的所有元素项会被统一收集起来,相当于把多个分区的列表合并成了一个“大列表”,然后再均匀地分配给所有并行任务。这种“均匀分配”的具体方法就是“轮询”(round-robin),与之前介绍的rebanlance数据传输方式类似,是通过逐一“发牌”的方式将状态项平均分配的。这种方式也叫作“平均分割重组”(even-split redistribution)。

算子状态中不会存在“键组”(key group)这样的结构,所以为了方便重组分配,就把它直接定义成了“列表”(list)这也就解释了,为什么算子状态中没有最简单的值状态(ValueState)。

2. 联合列表状态(UnionListState)

与ListState类似,联合列表状态也会将状态表示为一个列表。它与常规列表状态的区别在于,算子并行度进行缩放调整时对于状态的分配方式不同。

UnionListState的重点就在于“联合”(union)。在并行度调整时,常规列表状态是轮询分配状态项而联合列表状态的算子则会直接广播状态的完整列表。这样,并行度缩放之后的并行子任务就获取到了联合后完整的“大列表”,可以自行选择要使用的状态项和要丢弃的状态项。这种分配也叫作“联合重组”(union redistribution)。如果列表中状态项数量太多,为资源和效率考虑一般不建议使用联合重组的方式。

3. 广播状态(BroadcastState)

有时我们希望算子并行子任务都保持同一份“全局”状态,用来做统一的配置和规则设定。这时所有分区的所有数据都会访问到同一个状态,状态就像被“广播”到所有分区一样,这种特殊的算子状态,就叫作广播状态(BroadcastState)。

因为广播状态在每个并行子任务上的实例都一样,所以在并行度调整的时候就比较简单,只要复制一份到新的并行任务就可以实现扩展;而对于并行度缩小的情况,可以将多余的并行子任务连同状态直接砍掉——因为状态都是复制出来的,并不会丢失。

在底层,广播状态是以类似映射结构(map)的键值对(key-value)来保存的必须基于一个“广播流”(BroadcastStream)来创建。关于广播流,我们在第八章“广播连接流”的讲解中已经做过介绍,稍后还会在9.4节做一个总结。

4、1. CheckpointedFunction接口

在Flink中,对状态进行持久化保存的快照机制叫作“检查点”(Checkpoint)。于是使用算子状态时,就需要对检查点的相关操作进行定义,实现一个CheckpointedFunction接口。
CheckpointedFunction接口在源码中定义如下:

public interface CheckpointedFunction {
// 保存状态快照到检查点时,调用这个方法
void snapshotState(FunctionSnapshotContext context) throws Exception
// 初始化状态时调用这个方法,也会在恢复状态时调用
    void initializeState(FunctionInitializationContext context) throws Exception;
}

每次应用保存检查点做快照时,都会调用.snapshotState()方法,将状态进行外部持久化。而在算子任务进行初始化时,会调用. initializeState()方法。这又有两种情况:一种是整个应用第一次运行,这时状态会被初始化为一个默认值(default value);另一种是应用重启时,从检查点(checkpoint)或者保存点(savepoint)中读取之前状态的快照,并赋给本地状态。所以,接口中的.snapshotState()方法定义了检查点的快照保存逻辑,而. initializeState()方法不仅定义了初始化逻辑,也定义了恢复逻辑。

这里需要注意,CheckpointedFunction接口中的两个方法,分别传入了一个上下文(context)作为参数。不同的是,.snapshotState()方法拿到的是快照的上下文FunctionSnapshotContext,它可以提供检查点的相关信息,不过无法获取状态句柄;而. initializeState()方法拿到的是FunctionInitializationContext,这是函数类进行初始化时的上下文,是真正的“运行时上下文”。FunctionInitializationContext中提供了“算子状态存储”(OperatorStateStore)和“按键分区状态存储”(KeyedStateStore),在这两个存储对象中可以非常方便地获取当前任务实例中的Operator State和Keyed State。例如:

ListStateDescriptor<String> descriptor =
            new ListStateDescriptor<>(
                "buffered-elements",
                Types.of(String));
ListState<String> checkpointedState = context.getOperatorStateStore().getListState(descriptor);

我们看到,算子状态的注册和使用跟Keyed State非常类似,也是需要先定义一个状态描述器(StateDescriptor),告诉Flink当前状态的名称和类型,然后从上下文提供的算子状态存储(OperatorStateStore)中获取对应的状态对象。如果想要从KeyedStateStore中获取Keyed State也是一样的,前提是必须基于定义了key的KeyedStream,这和富函数类中的方式并不矛盾。通过这里的描述可以发现,CheckpointedFunction是Flink中非常底层的接口,它为有状态的流处理提供了灵活且丰富的应用。

3、代码实现

package com.example.chapter09;

import com.example.chapter05.ClickSource;
import com.example.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;


/**
 * 算子状态 OperatorStateStore
 * 自定义的SinkFunction 会在 CheckpointedFunction
 * 中进行数据缓存,然后统一发送到下游。这个例子演示了列表状态的平均分割重组(event-split redistribution)。
 */
public class BufferingSinkExample {
    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // TODO 检查点开启之后 Flink就会定时的周期性的为我们自动进行保存
        // enableCheckpointing 手动的保存的、需要用户手动触发的、用在有计划的重启、暂停、并行度调整、应用升级的场景
        env.enableCheckpointing(1000L);

        //TODO 检查点配置
        CheckpointConfig checkpointConfig = env.getCheckpointConfig();
        checkpointConfig.setCheckpointTimeout(60000L);
        checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        checkpointConfig.setMinPauseBetweenCheckpoints(500L);
        checkpointConfig.setMaxConcurrentCheckpoints(1); //并发1
        checkpointConfig.enableUnalignedCheckpoints(); //非对齐
        checkpointConfig.enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); //是否开启检查点的外部持久化
        checkpointConfig.setTolerableCheckpointFailureNumber(0); //是否允许当前检查点的失败、失败的话怎么样----默认不允许失败的

        //状态后端在代码里面的配置
          // env.setStateBackend(new HashMapStateBackend());
          // env.setStateBackend(new EmbeddedRocksDBStateBackend());

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                //乱序流的watermark生成
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        })
                );

        stream.print("input");
        //批量缓存输出
        stream.addSink(new BufferingSink(10));
        env.execute();

    }

    //自定义实现SinkFunction
    public static class BufferingSink implements SinkFunction<Event>, CheckpointedFunction {

        //定义当前类的属性、批量
        private final int threshold;

        public BufferingSink(int threshold) {
            this.threshold = threshold;
            this.bufferedElements = new ArrayList<>();
        }

        //存储数据
        private List<Event> bufferedElements;

        //定义一个算子状态
        private ListState<Event> checkPointState;

        /**
         * 每来一条数据都在这操作
         *
         * @param value
         * @param context
         * @throws Exception
         */
        @Override
        public void invoke(Event value, Context context) throws Exception {
            bufferedElements.add(value);// 缓存到列表

            //判断如果达到阈值,就批量写入
            if (bufferedElements.size() == threshold) {
                //用打印控制台模拟写入外部系统
                for (Event element : bufferedElements)
                    System.out.println(element);

                System.out.println("==============输出完毕===================");
                //TODO 写入外部系统的清空
                bufferedElements.clear();
            }

        }

        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {

            // 写入外部系统的清空 所以状态也不需要保存了
            checkPointState.clear();
            //对状态进行持久化,复制缓存的列表到列表状态
            for (Event element : bufferedElements) {
                checkPointState.add(element);
            }

        }

        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            //定义算子状态
            ListStateDescriptor<Event> descriptor = new ListStateDescriptor<Event>("buffered-elements", Event.class);
            checkPointState = context.getOperatorStateStore().getListState(descriptor);

            //如果从故障恢复、需要将listState中的所有元素复制到列表中
            //context.isRestored() 表示当前是否从故障恢复的----平均分割重组
            if (context.isRestored()) {
                for (Event element : checkPointState.get()) {
                    bufferedElements.add(element);
                }
            }

        }
    }
}

四、广播状态

算子状态中有一类很特殊,就是广播状态(Broadcast State)。从概念和原理上讲,广播状态非常容易理解:状态广播出去,所有并行子任务的状态都是相同的;并行度调整时只要直接复制就可以了。然而在应用上,广播状态却与其他算子状态大不相同。本节我们就专门来讨论一下广播状态的使用。

1、基本用法

让所有并行子任务都持有同一份状态,也就意味着一旦状态有变化,所以子任务上的实例都要更新。什么时候会用到这样的广播状态呢?

一个最为普遍的应用,就是“动态配置”或者“动态规则”。我们在处理流数据时,有时会基于一些配置(configuration)或者规则(rule)。简单的配置当然可以直接读取配置文件,一次加载,永久有效;但数据流是连续不断的,如果这配置随着时间推移还会动态变化,那又该怎么办呢?

一个简单的想法是,定期扫描配置文件,发现改变就立即更新。但这样就需要另外启动一个扫描进程,如果扫描周期太长,配置更新不及时就会导致结果错误;如果扫描周期太短,又会耗费大量资源做无用功。解决的办法,还是流处理的“事件驱动”思路——我们可以将这动态的配置数据看作一条流,将这条流和本身要处理的数据流进行连接(connect),就可以实时地更新配置进行计算了。

由于配置或者规则数据是全局有效的,我们需要把它广播给所有的并行子任务。而子任务需要把它作为一个算子状态保存起来,以保证故障恢复后处理结果是一致的。这时的状态,就是一个典型的广播状态。我们知道,广播状态与其他算子状态的列表(list)结构不同,底层是以键值对(key-value)形式描述的,所以其实就是一个映射状态(MapState)。

在代码上,可以直接调用DataStream的.broadcast()方法,传入一个“映射状态描述器”(MapStateDescriptor)说明状态的名称和类型,就可以得到一个“广播流”(BroadcastStream);进而将要处理的数据流与这条广播流进行连接(connect),就会得到“广播连接流”(BroadcastConnectedStream)。注意广播状态只能用在广播连接流中。

关于广播连接流,我们已经在8.2.2节做过介绍,这里可以复习一下:

MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(...);
BroadcastStream<Rule> ruleBroadcastStream = ruleStream
                        .broadcast(ruleStateDescriptor);
DataStream<String> output = stream
                 .connect(ruleBroadcastStream)
                 .process( new BroadcastProcessFunction<>() {...} );

这里我们定义了一个“规则流”ruleStream,里面的数据表示了数据流stream处理的规则,规则的数据类型定义为Rule。于是需要先定义一个MapStateDescriptor来描述广播状态,然后传入ruleStream.broadcast()得到广播流,接着用stream和广播流进行连接。这里状态描述器中的key类型为String,就是为了区分不同的状态值而给定的key的名称。

对于广播连接流调用.process()方法,可以传入“广播处理函数”KeyedBroadcastProcessFunction或者BroadcastProcessFunction来进行处理计算。广播处理函数里面有两个方法.processElement()和.processBroadcastElement(),源码中定义如下:

public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction {
...
    public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;
    public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
...
}

这里的.processElement()方法,处理的是正常数据流,第一个参数value就是当前到来的流数据;而.processBroadcastElement()方法就相当于是用来处理广播流的,它的第一个参数value就是广播流中的规则或者配置数据。两个方法第二个参数都是一个上下文ctx,都可以通过调用.getBroadcastState()方法获取到当前的广播状态;区别在于,.processElement()方法里的上下文是“只读”的(ReadOnly),因此获取到的广播状态也只能读取不能更改;而.processBroadcastElement()方法里的Context则没有限制,可以根据当前广播流中的数据更新状态。

Rule rule = ctx.getBroadcastState( new MapStateDescriptor<>("rules", Types.String, Types.POJO(Rule.class))).get("my rule");

通过调用ctx.getBroadcastState()方法,传入一个MapStateDescriptor,就可以得到当前的叫作“rules”的广播状态;调用它的.get()方法,就可以取出其中“my rule”对应的值进行计算处理。

2、代码实例

package com.example.chapter09;


import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.KeyedBroadcastProcessFunction;
import org.apache.flink.util.Collector;


/**
 * 广播状态
 * 应用实例
 *
 * TODO 广播流和广播状态的的应用实例
 *
 * 接下来我们举一个广播状态的应用案例。考虑在电商应用中,往往需要判断用户先后发生的行为的“组合模式”,比如“登录-下单”或者“登录-支付”,
 * 检测出这些连续的行为进行统计,就可以了解平台的运用状况以及用户的行为习惯。
 */
public class BehaviorPatternDetectExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        //用户行为数据流
        DataStreamSource<Action> actionStream = env.fromElements(
                new Action("Alice", "login"),
                new Action("Alice", "pay"),
                new Action("Bob", "login"),
                new Action("Bob", "order")
        );

        //行为模式流、基于它构建广播流
        DataStreamSource<Pattern> patternStream = env.fromElements(
                new Pattern("login", "pay"),
                new Pattern("login", "order")
        );

        //TODO 定义广播描述器
        MapStateDescriptor<Void, Pattern> descriptor = new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class));

        //创建广播流
        BroadcastStream<Pattern> broadcastStream = patternStream.broadcast(descriptor);

        //TODO 链接两条流进行处理 ---的到的广播连接流
        SingleOutputStreamOperator<Tuple2<String, Pattern>> matches = actionStream.keyBy(data -> data.userId)
                .connect(broadcastStream)
                .process(new PatternDetector());

        matches.print();
        env.execute();
    }


    /**
     * todo 实现自定义的keyedBroadcastProcessFunction
     * String key
     * Action 第一条流
     * Pattern 广播流的类型(第二条流)
     * Tuple2<String, Pattern> 输出的类型
     */
    public static class PatternDetector extends KeyedBroadcastProcessFunction<String, Action, Pattern, Tuple2<String, Pattern>> {

        //TODO 定义一个KeyedState,保存上一次用户行为
        ValueState<String> prevActionState;

        @Override
        public void open(Configuration parameters) throws Exception {
            prevActionState = getRuntimeContext().getState(new ValueStateDescriptor<String>("last-action", String.class));
        }


        /**
         * 数据流来了一个数据之后、怎么判断它是否符合定义的规则
         * @param value
         * @param ctx
         * @param out
         * @throws Exception
         */
        @Override
        public void processElement(Action value, KeyedBroadcastProcessFunction<String, Action, Pattern,
                Tuple2<String, Pattern>>.ReadOnlyContext ctx, Collector<Tuple2<String, Pattern>> out) throws Exception {

            //TODO 从广播状态中获取匹配模式
            ReadOnlyBroadcastState<Void, Pattern> broadcastState = ctx.getBroadcastState(
                    new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class)));

            Pattern pattern = broadcastState.get(null);

            //获取用户上一次的行为
            String prevAction = prevActionState.value();

            //判断是否匹配
            if (pattern != null && prevAction != null) {
                if (pattern.action1.equals(prevAction) && pattern.action2.equals(value.action)) {
                    //ctx.getCurrentKey() 当前用户ID
                    // 规则什么行为 pattern
                    out.collect(new Tuple2<>(ctx.getCurrentKey(), pattern));
                }

            }

            //更新状态
            prevActionState.update(value.action);

        }


        /**
         * 广播数据流的数据
         * @param value
         * @param ctx
         * @param out
         * @throws Exception
         */
        @Override
        public void processBroadcastElement(Pattern value, KeyedBroadcastProcessFunction<String, Action, Pattern,
                Tuple2<String, Pattern>>.Context ctx, Collector<Tuple2<String, Pattern>> out) throws Exception {

            //TODO 从上下文中获取广播状态、并用当前数据更新状态
            BroadcastState<Void, Pattern> patternState = ctx.getBroadcastState(
                    new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class)));
            patternState.put(null, value);

        }
    }

    /**
     * TODO 定义用户行为事件和模式的POJO类
     */
    public static class Action {
        public String userId;
        public String action;

        public Action(String userId, String action) {
            this.userId = userId;
            this.action = action;
        }

        public Action() {
        }

        @Override
        public String toString() {
            return "Action{" +
                    "userId='" + userId + '\'' +
                    ", action='" + action + '\'' +
                    '}';
        }
    }

    public static class Pattern {
        public String action1;
        public String action2;
        public Pattern() {

        }

        public Pattern(String action1, String action2) {
            this.action1 = action1;
            this.action2 = action2;
        }

        @Override
        public String toString() {
            return "Pattern{" +
                    "action1='" + action1 + '\'' +
                    ", action2='" + action2 + '\'' +
                    '}';
        }
    }
}

这里我们将检测的行为模式定义为POJO类Pattern,里面包含了连续的两个行为。由于广播状态中只保存了一个Pattern,并不关心MapState中的key,所以也可以直接将key的类型指定为Void,具体值就是null。在具体的操作过程中,我们将广播流中的Pattern数据保存为广播变量;在行为数据Action到来之后读取当前广播变量,确定行为模式,并将之前的一次行为保存为一个ValueState——这是针对当前用户的状态保存,所以用到了Keyed State。检测到如果前一次行为与Pattern中的action1相同,而当前行为与action2相同,则发现了匹配模式的一组行为,输出检测结果。

五、状态持久化和状态后端

在Flink的状态管理机制中,很重要的一个功能就是对状态进行持久化(persistence)保存,这样就可以在发生故障后进行重启恢复。Flink对状态进行持久化的方式,就是将当前所有分布式状态进行“快照”保存,写入一个“检查点”(checkpoint)或者保存点(savepoint)保存到外部存储系统中。具体的存储介质,一般是分布式文件系统(distributed file system)。

1、检查点(Checkpoint)

有状态流应用中的检查点(checkpoint),其实就是所有任务的状态在某个时间点的一个快照(一份拷贝)。简单来讲,就是一次“存盘”,让我们之前处理数据的进度不要丢掉。在一个流应用程序运行时,Flink 会定期保存检查点,在检查点中会记录每个算子的id和状态;如果发生故障,Flink就会用最近一次成功保存的检查点来恢复应用的状态,重新启动处理流程,就如同“读档”一样。

如果保存检查点之后又处理了一些数据,然后发生了故障,那么重启恢复状态之后这些数据带来的状态改变会丢失。为了让最终处理结果正确,我们还需要让源(Source)算子重新读取这些数据,再次处理一遍。这就需要流的数据源具有“数据重放”的能力,一个典型的例子就是Kafka,我们可以通过保存消费数据的偏移量、故障重启后重新提交来实现数据的重放。这是对“至少一次”(at least once)状态一致性的保证,如果希望实现“精确一次”(exactly once)的一致性,还需要数据写入外部系统时的相关保证。关于这部分内容我们会在第10章继续讨论。

除了检查点之外,Flink还提供了“保存点”(savepoint)的功能。保存点在原理和形式上跟检查点完全一样,也是状态持久化保存的一个快照;区别在于,保存点是自定义的镜像保存,所以不会由Flink自动创建,而需要用户手动触发。这在有计划地停止、重启应用时非常有用。

2、状态后端(State Backends)

检查点的保存离不开JobManager和TaskManager,以及外部存储系统的协调。在应用进行检查点保存时,首先会由JobManager向所有TaskManager发出触发检查点的命令;TaskManger收到之后,将当前任务的所有状态进行快照保存,持久化到远程的存储介质中;完成之后向JobManager返回确认信息。这个过程是分布式的,当JobManger收到所有TaskManager的返回信息后,就会确认当前检查点成功保存,如图9-5所示。而这一切工作的协调,就需要一个“专职人员”来完成。

在这里插入图片描述

在Flink中,状态的存储、访问以及维护,都是由一个可插拔的组件决定的,这个组件就叫作状态后端(state backend)。状态后端主要负责两件事:一是本地的状态管理,二是将检查点(checkpoint)写入远程的持久化存储。

1. 状态后端的分类

状态后端是一个“开箱即用”的组件,可以在不改变应用程序逻辑的情况下独立配置。Flink中提供了两类不同的状态后端,一种是“哈希表状态后端”(HashMapStateBackend),另一种是“内嵌RocksDB状态后端”(EmbeddedRocksDBStateBackend)。如果没有特别配置,系统默认的状态后端是HashMapStateBackend。

(1)哈希表状态后端(HashMapStateBackend)

这种方式就是我们之前所说的,把状态存放在内存里。具体实现上,哈希表状态后端在内部会直接把状态当作对象(objects),保存在Taskmanager的JVM堆(heap)上。普通的状态,以及窗口中收集的数据和触发器(triggers),都会以键值对(key-value)的形式存储起来,所以底层是一个哈希表(HashMap),这种状态后端也因此得名。

对于检查点的保存,一般是放在持久化的分布式文件系统(file system)中,也可以通过配置“检查点存储”(CheckpointStorage)来另外指定。

HashMapStateBackend是将本地状态全部放入内存的,这样可以获得最快的读写速度,使计算性能达到最佳;代价则是内存的占用。它适用于具有大状态、长窗口、大键值状态的作业,对所有高可用性设置也是有效的。

(2)内嵌RocksDB状态后端(EmbeddedRocksDBStateBackend)

RocksDB是一种内嵌的key-value存储介质,可以把数据持久化到本地硬盘。配置EmbeddedRocksDBStateBackend后,会将处理中的数据全部放入RocksDB数据库中,RocksDB默认存储在TaskManager的本地数据目录里。

与HashMapStateBackend直接在堆内存中存储对象不同,这种方式下状态主要是放在RocksDB中的。数据被存储为序列化的字节数组(Byte Arrays),读写操作需要序列化/反序列化,因此状态的访问性能要差一些。另外,因为做了序列化,key的比较也会按照字节进行,而不是直接调用.hashCode()和.equals()方法。

对于检查点,同样会写入到远程的持久化文件系统中。

EmbeddedRocksDBStateBackend始终执行的是异步快照,也就是不会因为保存检查点而阻塞数据的处理;而且它还提供了增量式保存检查点的机制,这在很多情况下可以大大提升保存效率。

由于它会把状态数据落盘,而且支持增量化的检查点,所以在状态非常大、窗口非常长、键/值状态很大的应用场景中是一个好选择,同样对所有高可用性设置有效。

2. 如何选择正确的状态后端

HashMap和RocksDB两种状态后端最大的区别,就在于本地状态存放在哪里:前者是内存,后者是RocksDB。在实际应用中,选择那种状态后端,主要是需要根据业务需求在处理性能和应用的扩展性上做一个选择。

HashMapStateBackend是内存计算,读写速度非常快;但是,状态的大小会受到集群可用内存的限制,如果应用的状态随着时间不停地增长,就会耗尽内存资源。

而RocksDB是硬盘存储,所以可以根据可用的磁盘空间进行扩展,而且是唯一支持增量检查点的状态后端,所以它非常适合于超级海量状态的存储。不过由于每个状态的读写都需要做序列化/反序列化,而且可能需要直接从磁盘读取数据,这就会导致性能的降低,平均读写性能要比HashMapStateBackend慢一个数量级。

我们可以发现,实际应用就是权衡利弊后的取舍。最理想的当然是处理速度快且内存不受限制可以处理海量状态,那就需要非常大的内存资源了,这会导致成本超出项目预算。比起花更多的钱,稍慢的处理速度或者稍小的处理规模,老板可能更容易接受一点。

3. 状态后端的配置

在不做配置的时候,应用程序使用的默认状态后端是由集群配置文件flink-conf.yaml中指定的,配置的键名称为state.backend。这个默认配置对集群上运行的所有作业都有效,我们可以通过更改配置值来改变默认的状态后端。另外,我们还可以在代码中为当前作业单独配置状态后端,这个配置会覆盖掉集群配置文件的默认值。

(1)配置默认的状态后端

在flink-conf.yaml中,可以使用state.backend来配置默认状态后端。

配置项的可能值为hashmap,这样配置的就是HashMapStateBackend;也可以是rocksdb,这样配置的就是EmbeddedRocksDBStateBackend。另外,也可以是一个实现了状态后端工厂StateBackendFactory的类的完全限定类名。

下面是一个配置HashMapStateBackend的例子:

# 默认状态后端
state.backend: hashmap
# 存放检查点的文件路径
state.checkpoints.dir: hdfs://namenode:40010/flink/checkpoints

这里的state.checkpoints.dir配置项,定义了状态后端将检查点和元数据写入的目录。

(2)为每个作业(Per-job)单独配置状态后端

每个作业独立的状态后端,可以在代码中,基于作业的执行环境直接设置。代码如下

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());

上面代码设置的是HashMapStateBackend,如果想要设置EmbeddedRocksDBStateBackend,可以用下面的配置方式:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new EmbeddedRocksDBStateBackend());

需要注意,如果想在IDE中使用EmbeddedRocksDBStateBackend,需要为Flink项目添加依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
    <version>1.13.0</version>
</dependency>

而由于Flink发行版中默认就包含了RocksDB,所以只要我们的代码中没有使用RocksDB的相关内容,就不需要引入这个依赖。即使我们在flink-conf.yaml配置文件中设定了state.backend为rocksdb,也可以直接正常运行,并且使用RocksDB作为状态后端。

五、本章总结

有状态的流处理是Flink的本质,所以状态可以说是Flink中最为重要的概念。之前聚合算子、窗口算子中已经提到了状态的概念,而通过本章的学习,我们对整个Flink的状态管理机制和状态编程的方式都有了非常详尽的了解。

本章从状态的概念和分类出发,详细介绍了Flink中的按键分区状态(Keyed State)和算子状态(Operator State)的特点和用法,并对广播状态(Broadcast State)做了进一步的展开说明。最后,我们还介绍了状态的持久化和状态后端,引出了检查点(checkpoint)的概念。检查点是一个非常重要的概念,是Flink容错机制的核心,我们将在下一章继续进行详细的讨论。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值