Flink笔记

常用指令

同步分发脚本至相同目录:xsync 文件名/

​ 自己编写的xsync分发脚本

rsync远程同步工具:避免复制相同内容

rsync -av flink-1.12.2/ dwc@hadoop103:/opt/Environment/flink-1.12.2/

复制scp -r 需要进入root权限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQsUdaZ5-1653008673131)(C:\Users\81553\AppData\Roaming\Typora\typora-user-images\image-20211225175227231.png)]

改名:mv 原/ 新

创建文件: mkdir 文件

修改环境变量 sudo vim /etc/profile.d/my_env.sh
运行环境变量 source /etc/profile

shutdown -h now 立刻进行关机
shutdown -h 1 一分钟后关机
shutdown -r now 立刻重启

rsync -av Environment_DataLake/ dwc@hadoop104:/opt/

分发环境变量:

sudo ./bin/xsync /etc/profile.d/my_env.sh

Flink-1.10.1部署

log中 日志查看报错

Standalone 模式

Flink集群启动和停止

./start-cluster.sh

./stop-cluster.sh

访问 hadoop102:8081 可以对 flink 集群和任务进行监控管理。

scala-2.12

0 DataStream

Flink 中的 DataStream 程序是对数据流实现转换(例如,筛选、更新状态、定义窗口、聚合)的常规程序。

数据流最初是从各种源(例如,消息队列、套接字流、文件)创建的。结果通过接收器返回,例如,接收器可以将数据写入文件或标准输出(例如命令行终端)

一套处理结构

/**
 * 执行环境
 */
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);



/**
 * 数据源
 */
 kafka等等
 
 
 
 /**
 * 算子处理、窗口
 */
 
 
 
 /**
 * 执行
 */
try {
    env.execute("job名字");
} catch (Exception e) {
    e.printStackTrace();
}
 
 

1 Environment

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4); //并行度

2 source

kafka作为输入

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

Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "hadoop102:9092");
properties.setProperty("group.id", "consumer-group");
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("auto.offset.reset", "latest");

// 读取数据  kafka是外部工具 引入依赖后 用addSource添加数据源
// 参数:主题topic 数据都是字符串SimpleStringSchema
DataStream<String> dataStream = env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));

// 打印输出 用流调用print
dataStream.print();

env.execute();

3 Transform

3.1 基本转换算子

map

map: 转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lj0Ng2Yb-1653008673132)(flink笔记.assets/Snipaste_2022-01-10_09-26-23.png)]

/**
 * 1. map转换方法,把读进来的数据String类型转换为长度Integer类型 再输出
 *  inputStream.map  数据源调用.map      new MapFunction<输入, 输出>    .length返回的是Integer整形
 */
DataStream<Integer> mapStream = inputStream.map(new MapFunction<String, Integer>() {
    @Override
    public Integer map(String value) throws Exception {
        //返回其长度.length()
        return value.length();
    }
});

flatmap

flatmap:拆分,来一个数据分成多个

// 2. flatmap,按逗号分字段
DataStream<String> flatMapStream = inputStream.flatMap(new FlatMapFunction<String, String>() {
    //flatmap方法 没有返回值类型return 用out.collect输出
    @Override
    public void flatMap(String value, Collector<String> out) throws Exception {
        String[] fields = value.split(",");  //用逗号分段 变成数组 后面用增强for循环拿出来
        for( String field: fields )
            out.collect(field);
    }
});

Filter

filter:过滤筛选

// 3. filter, 筛选sensor_1开头的id对应的数据
//    只是过滤 没有数据类型转换 所以只有String
DataStream<String> filterStream = inputStream.filter(new FilterFunction<String>() {
    @Override
    public boolean filter(String value) throws Exception {
        return value.startsWith("sensor_1");
    }
});
lambda VS 原写法
//  lambda写法
        DataStream<SensorReading> dataStream = inputStream.map( line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        } );

//  原写法
//        DataStream<SensorReading> dataStream = inputStream.map(new MapFunction<String, SensorReading>() {
//            @Override
//            public SensorReading map(String value) throws Exception {
//                String[] fields = value.split(",");
//                return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
//            }
//        });

3.2 聚合操作

分区 =》聚合

keyby

TransformTest1_Base

分区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Wrcz80i-1653008673133)(flink笔记.assets/Snipaste_2022-01-10_09-34-21.png)]

DataStreamKeyedStream:逻辑地将一个流拆分成不同的分区,相同 key 的元素会放到一个分区(但并不是分区里的元素全部都是相同的),在内部以 hash 的形式实现的。 严格来说算”数据传输“。

/**
 * 分组
 * .keyBy("id") 可以把属性字名 作为keyby的fields进行分组
 */
KeyedStream<SensorReading, Tuple> keyedStream = dataStream.keyBy("id");

滚动聚合算子

  • TransformTest2_RollingAggregation

这些算子可以针对 KeyedStream 的每一个支流做聚合。

⚫ sum() 求合 -----输入输出的类型不能改变

⚫ min() 求最值(仅值)

⚫ max() 求最值(仅值)

⚫ minBy() 最值的全部信息

⚫ maxBy() 最值的全部信息

例:TransformTest2_RollingAggregation (根据id进行keyby,再maxBy(“temperature”))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NBUVTRKz-1653008673133)(flink笔记.assets/Snipaste_2022-01-10_10-09-43.png)]

Reduce

  • TransformTest3_Reduce

KeyedStreamDataStream:一个分组数据流的聚合操作,合并当前的元素,是比前面的滚动聚合算子更加完善的方法

/**
 * reduce聚合,取最大的温度值,以及当前最新的时间戳
 * 注:类SingleOutputStreamOperator extends DataStream
 * value1 value2参数※: 在分区内的多个数据不断聚合中,value1是之前聚合的数据 value2是新来的数据
 * return:
 * value1.getId() => id号     value1、2的id都i是一样的
 * value2.getTimestamp()      最新的时间戳
 * Math.max                   最大的温度用.max方法比较
 */
SingleOutputStreamOperator<SensorReading> resultStream = keyedStream.reduce(new ReduceFunction<SensorReading>() {
    @Override
    public SensorReading reduce(SensorReading value1, SensorReading value2) throws Exception {
        return new SensorReading(value1.getId(), value2.getTimestamp(), Math.max(value1.getTemperature(), value2.getTemperature()));
    }
});

//  lambda写法
// keyedStream.reduce( (curState, newData) -> {
// return new SensorReading(curState.getId(), newData.getTimestamp(), Math.max(curState.getTemperature(), newData.getTemperature()));
// });

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eldISKJq-1653008673133)(flink笔记.assets/Snipaste_2022-01-10_10-51-07.png)]

3.3 多流转换算子

split

切分流

DataStreamSplitStream:根据某些特征把一个 DataStream 拆分成两个或者多个 DataStream。拆分出来的类型都是SplitStream类。

select

SplitStreamDataStream:从一个分出来的SplitStream 中获取(获取类型变为DataStream)。

3.4 Connect 和 CoMap

Connect操作把两个流stream1,stream2连接在一起(变为ConnectedStreams类),然后进行转换操作CoMap,CoFlatMap,才能进行处理

一国两制:连接一起过程时,两个流的数据类型可以不一样。转换操作时,数据要变成一样的

3.5 Union

DataStreamDataStream:对两个或者两个以上的 DataStream 进行 union 操作,产生一个包含所有 DataStream 元素的新 DataStream。

要求两条流是同样的数据类型

3.6 UDF函数

函数类

Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。例如

MapFunction, FilterFunction, ProcessFunction 等等。

lambda

富函数

        /**
         *  与前面不同 自己new 一个 MyMapper 实现更丰富的方法
         *  定义一个二元组<Tuple2<String, Integer>> 想获得其id(String) 其分区(integer)
         */
        DataStream<Tuple2<String, Integer>> resultStream = dataStream.map( new MyMapper() );

        resultStream.print();

        env.execute();
    }

    /**
     * 用自己定义的MyMapper实现更丰富的方法
     */
    public static class MyMapper extends RichMapFunction<SensorReading, Tuple2<String, Integer>>{
        @Override
        public Tuple2<String, Integer> map(SensorReading value) throws Exception {
            //获得id 获得getRuntimeContext运行时上下文来实现其他方法
            return new Tuple2<>(value.getId(), getRuntimeContext().getIndexOfThisSubtask());
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            // 初始化工作,一般是定义状态,或者建立数据库连接
            System.out.println("open");
        }

        @Override
        public void close() throws Exception {
            // 一般是关闭连接和清空状态的收尾操作
            System.out.println("close");
        }
    }
}

3.7 sink

stream.addSink(new MySink(xxxx))  

kafka输入+kafka输出

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka-0.11_2.12</artifactId>
    <version>1.10.1</version>
public class SinkTest1_Kafka {
    public static void main(String[] args) throws Exception{
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);


        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "hadoop102:9092");
        properties.setProperty("group.id", "consumer-group");
        properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("auto.offset.reset", "latest");

        // kafka生产
        DataStream<String> inputStream = env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));

        // 转换成SensorReading类型
        DataStream<String> dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2])).toString();
        });//用了toString 用来输出

        // kafka消费
        dataStream.addSink( new FlinkKafkaProducer011<String>("hadoop102:9092", "sinktest", new SimpleStringSchema()));

        env.execute();
    }
}
启动zk => 启动kk =》kafka目录
linux生产者
./bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic sensor
linux消费者
./bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic sinktest

生产者一条一条输入数据,经过处理(java),消费者消费

image-20211213213807512

生产

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N8D7WTam-1653008673134)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211213213341131.png)]

消费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pfl7nNSb-1653008673134)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211213213500010.png)]

Redis

现在只有scala2.11版本

但测试没有太用到2.12的特性,所以影响不大

<dependency>
    <groupId>org.apache.bahir</groupId>
    <artifactId>flink-connector-redis_2.11</artifactId>
    <version>1.0</version>
</dependency>

启动:

redis-server.exe redis.windows.conf

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xM8tcDHS-1653008673135)(flink笔记.assets/image-20220227163532401.png)]

注:如果启动报错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F7kjheVM-1653008673135)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211120193858687.png)]

解决方法:在命令行中运行

redis-cli.exe

127.0.0.1:6379>shutdown

not connected>exit

然后重新运行redis-server.exe redis.windows.conf,启动成功!

查看redis中的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggclGyvV-1653008673136)(flink笔记.assets/image-20220227163727909.png)]

kafka+redis

/**
 * @Description: kafka作为输入 输入至redis中
 *
 */
public class SinkTest2_KR {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);


        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "hadoop102:9092");
        properties.setProperty("group.id", "consumer-group");
        properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("auto.offset.reset", "latest");

        /**
         *  kafka生产
         */
        DataStream<String> inputStream = env.addSource( new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));

        /**
         * FLINK 基本算子
         */
        DataStream<SensorReading> dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        });

        // 定义参数1 jedis连接配置
        FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
                .setHost("localhost")
                .setPort(6379)
                .build();

        /**
         *   redis作为sink   RedisSink 2
         */
        dataStream.addSink( new RedisSink<>(config, new MyRedisMapper()));

        env.execute();
    }

    // 参数2 自定义RedisMapper
    public static class MyRedisMapper implements RedisMapper<SensorReading>{
        // 定义保存数据到redis的命令,存成Hash表,hset操作。 表名:temp  key:id    value:temperature
        //  保存数据到redis的命令
        @Override
        public RedisCommandDescription getCommandDescription() {
            return new RedisCommandDescription(RedisCommand.HSET, "temp");
        }
        //id 作为key
        @Override
        public String getKeyFromData(SensorReading data) {
            return data.getId();
        }
        //温度 作为value
        @Override
        public String getValueFromData(SensorReading data) {
            return data.getTemperature().toString();
        }
    }
}

注:代码中重要的两处位置,确定kafka输入的主题,和redis中的存储

​ 数据再redis存储以key-value形式 当作表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8YuI51F-1653008673136)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211219215811360.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pakyka45-1653008673137)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211219220014479.png)]

kafka输入数据:

./bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic ???

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjgMdHji-1653008673137)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211219215627158.png)]

添加数据后 redis中使用 keys * 才会显示表名

redis中查找数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E6RXEK6K-1653008673138)(C:\Users\段魏诚\AppData\Roaming\Typora\typora-user-images\image-20211219220306213.png)]

4 window

  • 把无线的数据流进行切分,得到有限的数据集进行处理(也就是得到有界流)
  • 将流数据分发到有限大小的桶(bucket)中进行分析

4.1 基本概念

Window 可以分成两类:

  • 计数窗口 CountWindow:按照指定的数据条数生成一个 Window,与时间无关。

    分为滚动计数窗口、滑动计数窗口

  • 时间窗口 TimeWindow:按照时间生成 Window。

    对于 TimeWindow,可以根据窗口实现原理的不同分成三类:滚动窗口(Tumbling Window)滑动窗口(Sliding Window)会话窗口(Session Window)

    1、滚动窗口(Tumbling Window)

    将数据依据固定的窗口长度对数据进行切片

    特点:时间对齐,窗口长度固定,没有重叠

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WDMXASuc-1653008673138)(flink笔记.assets/滚动窗口.png)]

    2、滑动窗口(Sliding Window)

    滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动 间隔组成。

    特点:时间对齐,窗口长度固定,可以有重叠

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lk5XGQgU-1653008673139)(flink笔记.assets/滑动窗口.png)]

    3、会话窗口(Session Window)

    由一系列事件组合一个指定时间长度的 timeout 间隙组成,类似于 web 应用的 session,也就是一段时间没有接收到新数据就会生成新的窗口。

    特点:时间无对齐。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2BQZeFN-1653008673139)(flink笔记.assets/会话窗口.png)]

4.2 Window API

  • packege-window

-------怎样去开一个窗口呢?

flink提供了简单的.timeWindow和.countWindow方法,用于定义时间窗口和计数窗口

必须先.keyby 再使用窗口window方法

如:
DataStream<Integer> resultStream = 
				  dataStream
				  .map(new MyMapper())
				  .keyBy()                             //先分
                  .timeWindow(Time.seconds(5));        //时间窗口    传一个时间参数是滚动窗口 传两个是滑动窗口
                  .reduce(new MyReduce());             //再做类似聚合的操作
.WindowAll方法,是把所有数据放在一个窗口做统计,相当于并行度为1,建议不使用
滚动时间窗口
.timeWindow(Time.seconds(5));  
滑动时间窗口
.timeWindow(Time.seconds(5),Time.seconds(5));  
会话窗口
.window(EventTimeSessionWindows.withGap(Time.minutes(5)))

4.3 window function

聚合

window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两 类:

  • 增量聚合函数(incremental aggregation functions) 每条数据到来就进行计算,保持一个简单的状态。

    典型的增量聚合函数有 ReduceFunction, AggregateFunction。

  • 全窗口函数(full window functions) 先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。不keyby

    ProcessWindowFunction 就是一个全窗口函数。

    如果要计算中位数 百分之多少等等 可以考虑用全窗口函数

4.3.1增量聚合函数 AggregateFunction

  • WindowTest1_TimeWindow

不同于reduce,aggregate会稍复杂

aggregate可以传三个泛型 输入 和 聚合 输出 的数据类型可以不一样 而reduce数据类型都是一样的,所以当需要处理并变化数据类型时,aggregate方法会更灵活

// 1. 增量聚合函数    做一个累加计数demo

DataStream<Integer> resultStream = dataStream.keyBy("id")                 //keyby分组
        .timeWindow(Time.seconds(15))    //时间窗口  传一个参数time是滚动窗口 传两个是滑动窗口
        .aggregate(new AggregateFunction<SensorReading, Integer, Integer>() {     //聚合  三个泛型<IN, ACC, OUT>:输入 聚合 输出
            //四个必须要实现的方法
            @Override
            public Integer createAccumulator() {                          //创建一个累加器 初始值为0
                return 0;
            }   

            @Override                                                  //怎样累加聚合  参数(传来的新数据、累加器)
            public Integer add(SensorReading value, Integer accumulator) {    
                return accumulator + 1;
            }  

            @Override
            public Integer getResult(Integer accumulator) {             //输出
                return accumulator;
            }   

            @Override
            public Integer merge(Integer a, Integer b) {
                return a + b;
            }  //想写可以写,这里一般用不到 一般在Session Window用来合并
        });

一个输入四条数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amErbufL-1653008673140)(flink笔记.assets/聚合1.png)]

输出

输出有id=7/10/1

按照id进行分组

然后计数

id=7 一个 id=10一个 id=1三个

4.3.2 全窗口函数

实现接口 :interface WindowFunction<IN, OUT, KEY, W extends Window>
in:input输入
out:输出
KEY: Tuple泛型,注:Tuple(二元组),keyby是啥它就是啥,
W: 窗口类型

增量聚合是来一个加一个,全窗口是全部攒齐再计算,且它可以输出更多信息,如:windowEnd

void apply(Tuple当前的key , Window窗口 , Iterable<>所有输入 , Collector<>输出)

如:Hotitems

/**
 * 实现自定义全窗口函数WindowItemCountResult
 * 本来单独使用全窗口函数会把所有数据都拿到  这里并不需要这样  只有把增量聚合的数据拿到就行
 * 实现接口 :interface WindowFunction<IN, OUT, KEY, W extends Window>
 * 其input是前面增量聚合的结果,  OUT输出是itemViewCount , KEY: Tuple,在这里是 itemId   W: 聚合的窗口
 * 注:Tuple,前面keyby是啥它就是啥,
 */
public static class WindowItemCountResult implements WindowFunction<Long , ItemViewCount , Tuple, TimeWindow>{
    
        //前面拿到的数据都在Iterable<Long> iterable里
        @Override
        public void apply(Tuple tuple, TimeWindow timeWindow, Iterable<Long> iterable, Collector<ItemViewCount> collector) throws Exception {
            Long itemId = tuple.getField(0);   //得到itemId:当前key就是按照itemId分的组,key只有一个itemId,所以Tuple只有一个itemId一元组,因此取Long itemId = tuple.getField(0)
            Long windowEnd = timeWindow.getEnd();    //用window拿,getEnd方法获得结束时间
            Long count = iterable.iterator().next();            //count已经在input数据里了
            collector.collect(new ItemViewCount(itemId, windowEnd, count));   //把这些数据包装为ItemViewCount 并collector.collect()输出
            //听全窗口函数apply方法
        }
}

4.4 计数窗口

前面都是时间窗口.timeWindow

下面是计数窗口countWindow 和 增量聚类.aggregate方法的demo

  • WindowTest2_CountWindow
        /**
         * 开计数窗口测试    统计温度值的平均温度demo
         */
        SingleOutputStreamOperator<Double> avgTempResultStream = dataStream.keyBy("id")
                .countWindow(10, 2)  //统计10个温度 隔2个滑动一次
                .aggregate(new MyAvgTemp());

        avgTempResultStream.print();

        env.execute();
    }

    /**
     * MyAvgTemp自定义聚合方法
     * 数据类型:   输入SensorReading    聚合Tuple2      输出Double
     * 聚合平均数  用二元组类型<温度求和  总个数>
     */
    public static class MyAvgTemp implements AggregateFunction<SensorReading, Tuple2<Double, Integer>, Double>{
        @Override
        public Tuple2<Double, Integer> createAccumulator() {
            return new Tuple2<>(0.0, 0);            //  创造一个累加器 初始的二元组为0.0和0
        }
        
        //处理方法
        //二元组第一个:f0   二元组第二个:f1
        //总温度值增加求和   总个数加1
        @Override
        public Tuple2<Double, Integer> add(SensorReading value, Tuple2<Double, Integer> accumulator) {
            return new Tuple2<>(accumulator.f0 + value.getTemperature(), accumulator.f1 + 1); 
        }

        //结果:  温度和 除以 总个数  = 平均值
        @Override
        public Double getResult(Tuple2<Double, Integer> accumulator) {
            return accumulator.f0 / accumulator.f1;
        }

        @Override
        public Tuple2<Double, Integer> merge(Tuple2<Double, Integer> a, Tuple2<Double, Integer> b) {
            return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);   //把求和合并在一起  个数合并在一起
        }
    }
}

4.5 其他API

  • .trigger() —— 触发器

定义 window 什么时候关闭,定义什么时候触发计算并输出结果

  • evitor() —— 移除器

定义移除某些数据的逻辑(对某些不需要的数据进行移除,类似过滤)

  • .allowedLateness(延迟时间) —— 允许处理迟到的数据的时间(即先输出一个近似结果值,窗口未关闭,接下来时间进入的数据可以继续进行计算),所谓的迟到数据到底是什么,这涉及到时间语义。

  • .sideOutputLateData() —— 将迟到的数据放入侧输出流

  • .getSideOutput() —— 获取侧输出流

4.6 总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywBbQeJG-1653008673140)(flink笔记.assets/window.png)]

5 时间语义与 Wartermark

5.1 时间语义

在分布式中,存在延迟导致的乱序数据,本来应该是前面的数据延迟到了后面,导致:到达的时间便不是发出的时间,处理数据的顺序也不是刚开始的顺序,因此定义时间语义

Event Time 事件时间:是事件创建的原始时间,数据最初的顺序时间戳。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。

Ingestion Time:是数据进入 Flink 系统的时间。 具体来说是进入source的时间,但然后还有keyby、sink的过程,时间顺序又可能发生变化,还有window计算的时间(Processing Time)

Processing Time 处理时间:(window计算的时间)是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是 Processing Time。

​ 例子

5.1.1 事件时间EventTime引入

  • WindowTest3_EventTimeWindow

Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。

如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所示:

窗口的关闭就决定于EventTime
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 设置流式时间语义  从调用时刻开始给env创建的每一个stream追加时间特征 
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

后面还需要设置定义把类中的什么字段认定为时间戳

5.2 Watermark

流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的.

虽然大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的,

但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。

那么此时出现一个问题,一旦出现乱序,如果只根据 eventTime 决定 window 的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算了,这个特别的机制,就是 Watermark。

  • watermark是一种数据,它里面带有时间戳的属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgCH3i9s-1653008673141)(flink笔记.assets/image-20220124212037361.png)]

5.2.1 Watermark 的引入

  • watermark设置太久–实时性差

  • watermark设置太久–输出不准确,但flink处理迟到数据的机制可以解决这个问题

  • watermark周期性生成:系统会周期性的将生成 watermark ,不频繁。但对于数据稠密(大数据)时,建议使用周期性,所以代码中默认使用周期性

    默认周期是 200 毫秒。自定义设置周期:

    env.getConfig().setAutoWatermarkInterval(100); 设置周期性watermark的周期 默认200
    
  • watermark间断式生成:对每条数据进行筛选和处理,频繁,时效性强

引入watermark(分为乱序和升序):

//乱序数据
// 提取时间戳和生成watermark     BoundedOutOfOrdernessTimestampExtractor乱序数据时间戳提取器
//    Time.seconds(2) 参数为watermark的值
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
    @Override
    public long extractTimestamp(SensorReading element) {    //生成时间戳的方法   生成毫秒数
        return element.getTimestamp() * 1000L;  //demo中的txt文件单位是秒 要保持一致所以乘1000
    } 
});
// 升序数据设置事件时间和watermark (特殊情况下数据是按照顺序的)
// 提取时间戳和生成watermark   AscendingTimestampExtractor升序
//                .assignTimestampsAndWatermarks(new AscendingTimestampExtractor<SensorReading>() {
//                    @Override
//                    public long extractAscendingTimestamp(SensorReading element) { //生成时间戳的方法 毫秒数
//                        return element.getTimestamp() * 1000L;   //demo中的txt文件单位是秒 要保持一致所以乘1000
//                    }
//                })

5.3 测试

WindowTest3_EventTimeWindow

5.3.1 无延迟数据

watermark设置为2,在时间戳为212时关闭窗口,212-2=210,窗口设置为15秒,所以这个窗口在 [195,210)

为什么不是[199,214)呢?   --57节  
此方法计算窗口起始点:
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
    return  timestamp -   (timestamp - offset + windowSize) % windowSize;
}
timestamp当前时间戳   offset偏移量(未设置)  windowSize窗口长度
结果为1547718195

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x64f02nQ-1653008673141)(flink笔记.assets/watermark测试2.png)]

触发窗口关闭后,输出温度最小值(按照id分组后,每个id的最小值放在第一个窗口里)

5.3.2 三重保证-允许延迟数据的引入

/**
 * 设置流式时间语义  EventTime 从调用时刻开始给env创建的每一个stream追加时间特征  窗口的关闭就决定于EventTime
 */
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
/**
*  乱序数据设置时间戳和watermark=2
*/
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
    @Override
    public long extractTimestamp(SensorReading element) {
        return element.getTimestamp() * 1000L;  //txt文件数据是秒 返回需要毫秒 所以×1000
    }
});

/**
 * 侧输出流
 */
OutputTag<SensorReading> outputTag = new OutputTag<SensorReading>("late") {
};

/**
 *  基于事件时间的开窗聚合,统计15秒内温度的最小值
 */
SingleOutputStreamOperator<SensorReading> minTempStream = dataStream.keyBy("id")
        .timeWindow(Time.seconds(15))       //15秒的窗口
        .allowedLateness(Time.minutes(1))  //允许延迟数据的引入: 1分钟内  进入第一个桶
        .sideOutputLateData(outputTag)     //但1分组后又来了数据怎么办?  放到侧输出流
        .minBy("temperature");        //算子 选择min温度

minTempStream.print("minTemp");  //打印最小温度 命名为minTemp
minTempStream.getSideOutput(outputTag).print("late");  //侧输出流 的延迟的数据 打印

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjtLarTg-1653008673141)(flink笔记.assets/watermark测试3.png)]

第一个窗口 [195,210) 结束后,一分钟内的延迟数据使209数据进入,当272数据进入(210+60 = 270) 标志1分钟延迟数据结束,同时其他窗口的时间戳也过了,其他窗口输出

再延迟的数据放在测输出流late输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSi2ZwXW-1653008673142)(flink笔记.assets/watermark测试4.png)]

5.4 ProcessFunction API 底层API ??????????

可以访问时间 戳、watermark 以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。 Process Function 用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的 window 函数和转换算子无法实现)。例如,Flink SQL 就是使用 Process Function 实 现的。

5.4.1 KeyedProcessFunction⭐

  • ProcessTest1_KeyedProcessFunction

KeyedProcessFunction最常用

KeyedProcessFunction 用来操作 KeyedStream。KeyedProcessFunction 会处理流 的每一个元素,输出为 0 个、1 个或者多个元素。所有的 Process Function 都继承自 RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。

extends KeyedProcessFunction
open              具体定义状态
processElement    每一条数据进来进行的操作
				  context.timerService().registerEventTimeTimer(时间);  定时器是以时间戳作为标志的
				  比如三次注册了同样时间触发的定时器  属于一个定时器
onTimer   定时器触发    

5.4.2 侧输出流

6 Flink状态管理

计算不仅依赖于一个数据,要靠众多数据计算处理(如求和、最小值、聚合),这些数据就称作为Flink中的状态。

  • Flink又两种类型的状态:算子状态 Operator State / 键控状态(分组状态)keyed State

  • 状态后端 State Backends

在flink中,有一个特定的算子就有一个特定的任务,就有状态。状态始终与特定算子相关联。

为了使运行时的Flink了解算子的状态,算子需要预先注册其状态。

6.1 算子状态 Operator State

StateTest1_OperatorState

算子状态的作用范围为算子任务 (只有当前任务可以访问到它)

相对用的少一点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tLVomKzI-1653008673142)(flink笔记.assets/image-20220125114328714.png)]

Flink 为算子状态提供三种基本数据结构:

  • 列表状态(List state)

将状态表示为一组数据的列表。

  • 联合列表状态(Union list state)

也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保 存点(savepoint)启动应用程序时如何恢复。

  • 广播状态(Broadcast state)

如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。

6.2 键控状态(分组状态)keyed State

进行了key分组,只有当前key对应的数据可以访问到当前的状态

Flink 的 Keyed State 支持以下数据类型:

  • ValueState保存单个的值,值的类型为 T。

get 操作: ValueState.value()

set 操作: ValueState.update(T value)

  • ListState保存一个列表,列表里的元素的数据类型为 T。基本操作如下:

ListState.add(T value)

ListState.addAll(List values)

ListState.get()返回 Iterable

ListState.update(List values)

  • MapState保存 Key-Value 对

MapState.get(UK key)

MapState.put(UK key, UV value)

MapState.contains(UK key)

MapState.remove(UK key)

  • ReducingState

  • AggregatingState

State.clear()是清空操作

6.3 状态后端 State Backends

状态都是在本地内存中保存维护的

对状态的存储、访问、维护都是由一个可插入的组件决定的,这个组件叫做状态后端

状态后端主要负责:本地的状态管理、将检查点状态(checkpoint)写入远程存储

  • 三个不同类型的状态后端:

  • MemoryStateBackend

内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储 在 TaskManager 的 JVM 堆上;而将 checkpoint 存储在 JobManager 的内存中。

  • FsStateBackend

将 checkpoint 存到远程的持久化文件系统(FileSystem)上。而对于本地状 态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上。

  • RocksDBStateBackend

将所有状态序列化后,存入本地的 RocksDB 中存储。

StateTest4_FaultTolerance 
// 1. 三种状态后端配置
env.setStateBackend( new MemoryStateBackend());
env.setStateBackend( new FsStateBackend(""));
env.setStateBackend( new RocksDBStateBackend(""));
注:RocksDBStateBackend 的支持并不直接包含在 flink 中,需要引入依赖:
<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-statebackend-rocksdb_2.12</artifactId>
	<version>1.10.1</version>
</dependency>

6.4 容错机制

  • StateTest4_FaultTolerance

容错机制的核心就是检查点

6.4.1 一致性检查点 checkpoint

6.4.2 从检查点恢复状态

6.4.3 Flink检查点算法

Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。

6.4.4 保存点 save points

可以基于保存点,更新flink版本,不影响数据

6.5 状态一致性

一致性检查点

端到端状态一致性

端到端的精确一次保证

flink+kafka端到端一致性的保证

在流处理中,一致性可以分为 3 个级别:

⚫ at-most-once: 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。同样的还有 udp。

⚫ at-least-once: 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。

⚫ exactly-once: 这指的是系统保证在发生故障后得到的计数结果与正确值一致。

端到端状态一致性

端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;

⚫ 内部保证 —— 依赖 checkpoint | flink使用一种轻量级快照机制-----检查点checkpoint来保证exactly-once语义

⚫ source 端 —— 需要外部源可重设数据的读取位置

⚫ sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统 (包含:幂等写入Idempotent、事务性写入Transactional )

幂等写入:一个操作可以执行多次,但但只会导致一次结果更改,这样重复写入就不会存在问题了

事务写入:实现方式有两种:预写日志WAL、两阶段提交2PC

flink+kafka端到端一致性的保证

端到端的状态一致性的实现,需要每一个组件都实现,对于 Flink + Kafka 的数据管道系统(Kafka 进、Kafka 出)而言,各组件怎样保证 exactly-once 语义呢?

  • 内部 —— 利用 checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
  • source —— kafka consumer 作为 source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
  • sink —— kafka producer 作为 sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction

Exactly-once两阶段提交步骤:

  • 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
  • jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
  • sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
  • jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
  • sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
  • 外部 kafka 关闭事务,提交的数据可以正常消费了。

7 Table API 与 Flink SQL

7.1 简介

​ 在Flink 提供的多层级 API 中,核心是 DataStream API,这是我们开发流处理应用的基本途径;底层则是所谓的处理函数(process function),可以访问事件的时间信息、注册定时器、自定义状态,进行有状态的流处理。DataStream API 和处理函数比较通用,有了这些 API,理论上我们就可以实现所有场景的需求了。

​ 不过在企业实际应用中,往往会面对大量类似的处理逻辑,所以一般会将底层 API 包装成更加具体的应用级接口。怎样的接口风格最容易让大家接收呢?作为大数据工程师,我们最为熟悉的数据统计方式,当然就是写 SQL 了。

​ Flink 的 Table & SQL API 可以处理 SQL 语言编写的查询语句,但是这些查询需要嵌入用 Java 或 Scala 编写的表程序中。此外,这些程序在提交到集群前需要用构建工具打包。这或多或少限制了 Java/Scala 程序员对 Flink 的使用。

​ SQL 客户端 的目的是提供一种简单的方式来编写、调试和提交表程序到 Flink 集群上,而无需写一行 Java 或 Scala 代码。SQL 客户端命令行界面(CLI) 能够在命令行中检索和可视化分布式应用中实时产生的结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3jvhgLw-1653008673142)(flink笔记.assets/image-20220319170309730.png)]

Table API不像DataStream那样能拿到底层信息,也不像SQL那样简单易用,所以实际开放中,要么用SQL,要么用DataStream。此章节将以SQL作为重点进行讲解。

Table API 和 SQL 也依然不算稳定,接口用法还在不停调整和更新,要时刻关注官网的变化。

7.2 依赖

我们想要在代码中使用 Table API,必须引入相关的依赖。

<flink.version>1.13.3</flink.version>
<scala.binary.version>2.11</scala.binary.version>





-----------------------------------------------------
<dependency> 
    <groupId>org.apache.flink</groupId> 
	<artifactId>flink-table-api-java-bridge_${scala.binary.version}</artifactId> 
    <version>${flink.version}</version> 
</dependency> 
-------------------------------------------------
bridge桥接器,主要就是负责Table API转换为层DataStream API的连接支持,本质来讲,还是在使用datastream



如果我们希望在本地的集成开发环境(IDE)里运行 Table API 和 SQL,还需要引入以下依赖: 
------------------------------------------------------
<dependency> 
    <groupId>org.apache.flink</groupId> 
<artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId> 
    <version>${flink.version}</version> 
</dependency> 
<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId> 
    <version>${flink.version}</version> 
</dependency> 
----------------------------------------------------
这里主要添加的依赖是一个“计划器”(planner),它是 Table API 的核心组件,负责提供运行时环境,并生成程序的执行计划。这里我们用到的是新版的 blink planner。由于 Flink 安装包的 lib 目录下会自带 planner,所以在生产集群环境中提交的作业不需要打包这个依赖。而在 Table API 的内部实现上,部分相关的代码是用 Scala 实现的,所以还需要额外添加一个 Scala 版流处理的相关依赖。 


        <!-- 引入Flink相关依赖-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

7.3 基本API

7.3.1 程序架构

在 Flink 中,Table API 和 SQL 可以看作联结在一起的一套 API,这套 API 的核心概念就是“表”(Table)。在我们的程序中,输入数据可以定义成一张表;然后对这张表进行查询,就可以得到新的表,这相当于就是流数据的转换操作;最后还可以定义一张用于输出的表,负责将处理结果写入到外部系统。

我们可以看到,程序的整体处理流程与 DataStream API 非常相似,也可以分为读取数据源(Source)、转换(Transform)、输出数据(Sink)三部分;只不过这里的输入输出操作不需要额外定义,只需要将用于输入和输出的表定义出来,然后进行转换查询就可以了。

程序基本架构如下:

// 1 创建表环境 
TableEnvironment tableEnv = ...; 
  
// 2 输入表,连接外部系统读取数据 
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector' = ... )"); 
 
// 3 输出表,连接到外部系统输出 
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector' = ... )"); 
 
// 4 视图
    // 4.1 用SQL对表进行查询转换  
Table table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... "); 
    // 4.2 用Table API对表进行查询转换   
Table table2 = tableEnv.from("inputTable").select(...); 

// 5 将得到的结果写入输出表 

7.3.2 创建表环境

// 1 表环境(TableEnvironment) 基于表创建环境  基于blink版本planner进行流处理  并行度会默认为电脑的核数
EnvironmentSettings settings = EnvironmentSettings 
    .newInstance() 
    .inStreamingMode()    // 使用流处理模式 
    .useBlinkPlanner()
    .build(); 
 
TableEnvironment tableEnv = TableEnvironment.create(settings); 


// 2 流式表环境( StreamTableEnvironment ) 基于流来创建表环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

对于 Flink 这样的流处理框架来说,使用 Table API 和 SQL 需要一个特别的运行时环境,这就是所谓的“表环境”(TableEnvironment)。

它主要负责:

(1)注册 Catalog 和表;

(2)执行 SQL 查询;

(3)注册用户自定义函数(UDF);

(4)DataStream 和表之间的转换。

​ 这里的 Catalog 就是“目录”,与标准 SQL 中的概念是一致的,主要用来管理所有数据库(database)和表(table)的元数据

(metadata)。通过 Catalog 可以方便地对数据库和表进行查询的管理,所以可以认为我们所定义的表都会“挂靠”在某个目录下,这样就

可以快速检索。在表环境中可以由用户自定义 Catalog,并在其中注册表和自定义函数(UDF)。默认的 Catalog 就叫作

default_catalog。

7.3.3 创建表

为了方便地查询表,表环境中会维护一个目录(Catalog)和表的对应关系。所以表都是通过 Catalog 来进行注册创建的。表在环境中有一个唯一的 ID,由三部分组成:目录(catalog)名,数据库(database)名,以及表名。在默认情况下,目录名为 default_catalog,数据库名为 default_database。所以如果我们直接创建一个叫作 MyTable 的表,它的 ID 就是:

default_catalog.default_database.MyTable

具体创建表的方式,有通过连接器(connector)和虚拟表(virtual tables)两种

没有定义 Catalog 和 Database ,所以都是默认的,表的完整 ID 就是 default_catalog.default_database.MyTable。如果希望使用自定义的目录名和库名,可以在环境中进行设置:

tEnv.useCatalog("custom_catalog");

tEnv.useDatabase("custom_database");

连接器

最直观的创建表的方式,就是通过连接器(connector)连接到一个外部系统,然后定义出对应的表结构。例如我们可以连接到 Kafka 或者文件系统,将存储在这些外部系统的数据以“表” 的形式定义出来,这样对表的读写就可以

通过连接器转换成对外部系统的读写了。

//2.1 创建输入表    表字段可以不一样--重命名
String creatDDl = "CREATE TABLE clickTables(" +
        "user_name STRING," +
        "url STRING," +
        "ts BIGINT" +
        ")WITH (" +
        " 'connector' =  'filesystem', " +
        " 'path' = 'input/clicks.csv', " +
        " 'format' = 'csv' " +
        ")";
tableEnv.executeSql(creatDDl);   //执行sql语句creatDDl

或简写
tableEnv.executeSql("..............")

虚拟表(Virtual Tables)

情景:

1 在流的环境,把datastream转换为table,表无法和sql一样直接使用,需要转化为虚拟表。

2 通过创建视图的表无法直接在sql中使用,要创建虚拟表再使用

如:
把datastream转换为table的表
Table eventTable = tableEnv.fromDataStream(eventStream);

eventTable无法在sql中直接使用,eventTable只是一个java对象,需要用拼接方式+
Table resultTable1 = tableEnv.sqlQuery("select url, user from " + eventTable);

但是转换为虚拟表,就可以在sql中直接使用
tableEnv.createTemporaryView("eventTable", eventTable); 

也叫作创建“虚拟视图”,视图之所以是“虚拟”的,是因为我们并不会直接保存这个表的内容,并没有“实体”;只是在用到这张表的时候,会将它对应的查询语句嵌入到 SQL 中。 本质就像视图一样,所以用.createTemporaryView()

虚拟表也可以让我们在 Table API 和 SQL 之间进行自由切换。一个 Java 中的 Table 对象可以直接调用 Table API 中定义好的查询转换方法,得到一个中间结果表;这跟对注册好的表直接执行 SQL 结果是一样的。

7.3.4 表的查询(视图)和插入

  • 两种查询方式:

  • 直接写sql

  • 调用table api

直接写sql

// 已经基于表创建表环境 创建输入表...

// 1 sql 视图
Table viewTable = tableEnv.sqlQuery("select url, user_name from input");

   // 插入到输出表output    这种方法可以省略env.execute()
viewTable.executeInsert("output");
   // 如果要继续使用视图在sql语句中,需要把它创建为虚拟表

// 2 视图和插入 写在一起
tableEnv.executeSql ( "INSERT INTO output " + 
    "SELECT user_name, url " + 
    "FROM input " + 
    "WHERE user = 'Alice' " 
  ); 

调用table api

//已经基于表创建表环境 创建输入表...

// 调用table api
Table input = tableEnv.from("input");  //把注册在表环境的input(后面)转化为java对象(前面) 再select
Table resultTable = input.where($("user_name").isEqual("Bob"))
        .select($("user_name"), $("url"));
        
        // 插入到输出表output
        resultTable.executeInsert("output");

7.3.5 表和流的转化 134⭐

在应用的开发过程中,我们测试业务逻辑一般不会直接将结果直接写入到外部系统,而是在本地控制台打印输出。对于 DataStream 这非常容易,直接调用 print() 方法就可以看到结果数据流的内容了;但对于 Table 就比较悲剧——它没有提供 print()方法。

这该怎么办呢?在 Flink 中可以将 Table 再转换成 DataStream,然后进行打印输出。这就涉及了表和流的转换。

要求在流的环境下

1 将表(Table)转换成流(DataStream)

// 建表连接mysql
tableEnv.executeSql("CREATE TABLE mysql_binlog ( .........................."
// 查询数据 
Table table = tableEnv.sqlQuery("select * from mysql_binlog");
// 将动态表table转换为流
DataStream<Tuple2<Boolean, Row>> retractStream = tableEnv.toRetractStream(table, Row.class);
retractStream.print();
// 将表转换成数据流,打印输出
tableEnv.toDataStream(table).print("result1");

但如果涉及复杂环境,聚合计数转换等等toDataStream()就不行了。toDataStream()默认为添加操作,updata无法解析

tableEnv.toChangelogStream(table).print("result2");

7.3.6 connector配置参数

https://ververica.github.io/flink-cdc-connectors/master/content/connectors/mysql-cdc.html

7.4 时间属性和窗口

7.4.1 事件时间

实际应用中,最常用的就是事件时间。在事件时间语义下,允许表处理程序根据每个数据中包含的时间戳(也就是事件发生的时间)来生成结果。

事件时间语义最大的用途就是处理乱序事件或者延迟事件的场景。我们通过设置水位线(watermark)来表示事件时间的进展,而水位线可以根据数据的最大时间戳设置一个延迟时间。这样即使在出现乱序的情况下,对数据的处理也可以获得正确的结果。

为了处理无序事件,并区分流中的迟到事件。Flink 需要从事件数据中提取时间戳,并生成watermark,用来推进事件时间的进展。

事件时间属性可以在创建表 DDL 中定义,也可以在数据流和表的转换中定义:

(1) 在创建表的 DDL 中定义

在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个字段,通过 WATERMARK 语句来定义事件时间属性。

WATERMARK 语句主要用来定义水位线(watermark)的生成表达式,这个表达式会将带有事件时间戳的字段标记为事件时间属性,并在它基础上给出水位线的延迟时间。具体定义方式如下: 

CREATE TABLE EventTable(   
	user STRING,   
	url STRING,   
	ts TIMESTAMP(3),   //  TIMESTAMP(3)精确到毫秒
    WATERMARK FOR ts AS ts - INTERVAL '5' SECOND 
) WITH (   ... 
); 

**注:WATERMARK FOR ts AS ts - INTERVAL '5' SECOND 
事件时间eventtime:
  	 把 ts 字段定义为事件时间eventtime,
基于 ts 设置水位线延时时间:   
     格式=>  INTERVAL '数值' 时间单位 
基于 ts 设置 watermark:  
     事件时间ts 减 水位线的延迟时间 = watermark    格式=>  ts - INTERVAL '5' SECOND 



**注:
定义事件时间的字段必须是TIMESTAMP或TIMESTAMP_LTZ       //(TIMESTAMP_LTZ 是指带有本地时区信息的时间戳)
如果类型不符合呢?如下:
CREATE TABLE events(   
  user STRING,   
  url STRING,   
  ts BIGINT,                                            //****ts类型不符合,需要转换   假设ts目前单位是毫秒1
) WITH (   ... 
); 
这种ts,类型不符合,需要转换:
方法:et AS TO_TIMESTAMP(ts, 3),       //把ts转为TIMESTAMP(3)  记为et
但是括号()中传入的类型必须是string,且必须是秒,所以用方法FROM_UNIXTIME(ts/1000),把ts转为string

  ts BIGINT,    //类型不符合,需要转换
  et AS TO_TIMESTAMP(FROM_UNIXTIME(ts/1000), 3),      
  WATERMARK FOR et AS et - INTERVAL '5' SECOND   


(2) 在数据流转换为表时定义

我们调用 fromDataStream() 方法创建表时,可以追加参数来定义表中的字段结构;这时可以给某个字段加上.rowtime() 后缀,就表示将当前字段指定为事件时间属性。这个字段可以是数据中本不存在、额外追加上去的“逻辑字段”,就像之前 DDL 中定义的第二种情况;也可以是本身固有的字段,那么这个字段就会被事件时间属性所覆盖,类型也会被转换为 TIMESTAMP。不论那种方式,时间属性字段中保存的都是事件的时间戳(TIMESTAMP 类型)。

需要注意的是,这种方式只负责指定时间属性,而时间戳的提取和水位线的生成应该之前就在 DataStream 上定义好了。由于 DataStream 中没有时区概念,因此 Flink 会将事件时间属性解析成不带时区的 TIMESTAMP 类型,所有的时间值都被当作 UTC 标准时间。

// 方法一: 
// 流中数据类型为二元组Tuple2,包含两个字段;需要自定义提取时间戳并生成水位线 
DataStream<Tuple2<String, String>> stream = 
inputStream.assignTimestampsAndWatermarks(...);

// 声明一个额外的逻辑字段作为事件时间属性 
Table table = tEnv.fromDataStream(stream, $("user"), $("url"), 
$("ts").rowtime());

 
// 方法二: 
// 流中数据类型为三元组Tuple3,最后一个字段就是事件时间戳 DataStream<Tuple3<String, String, Long>> stream = inputStream.assignTimestampsAndWatermarks(...); 
 
// 不再声明额外字段,直接用最后一个字段作为事件时间属性 
Table table = tEnv.fromDataStream(stream, $("user"), $("url"), 
$("ts").rowtime());


7.4.2 处理时间

​ 相比之下处理时间就比较简单了,它就是我们的系统时间,使用时不需要提取时间戳(timestamp)和生成水位线(watermark)。因此在定义处理时间属性时,必须要额外声明一个字段,专门用来保存当前的处理时间。

(1)在创建表的 DDL 中定义

CREATE TABLE EventTable(   
	user STRING,   
	url STRING,   
	ts AS PROCTIME() 
) WITH (   ... 
); 

调用系统内置的 PROCTIME()函数来指定当前的处理时间属性,返回的类型是 TIMESTAMP_LTZ。

7.4.3 窗口

把无线的数据流切割为有限的数据集(桶)

7.4.3.1 介绍

**flink1.12的老版本(Group Window,老版本) **

调用 TUMBLE()、HOP()、SESSION() //滚动滑动会话窗口

滚动窗口为例:

TUMBLE(ts, INTERVAL ‘1’ HOUR)

**flink1.13新版本(Windowing TVFs,新版本) **

思想:把原始的表进行扩展后返回,增加一些新列

目前 Flink 提供了以下几个窗口 TVF:

  • 滚动窗口(Tumbling Windows);
  • 滑动窗口(Hop Windows,跳跃窗口);
  • 累积窗口(Cumulate Windows); //新增
  • 会话窗口(Session Windows,目前尚未完全支持)。

在窗口 TVF 的返回值中,除去原始表中的所有列,还增加了用来描述窗口的额外 3 个列:

窗口起始点”(window_start)、“窗口结束点”(window_end)、“窗口时间”(window_time)

起始点和结束点比较好理解,

这里的“窗口时间”指的是窗口中的时间属性,它的值等于 window_end - 1ms,所以相当于是窗口中能够包含数据的最大时间戳。

7.4.3.2 滚动窗口TUMBLE

滚动窗口在 SQL 中的概念与 DataStream API 中的定义完全一样,是长度固定、时间对齐、无重叠的窗口,一般用于周期性的统计计算。

​ 在 SQL 中通过调用 TUMBLE()函数就可以声明一个滚动窗口,只有一个核心参数就是窗口大小(size)。

​ 在 SQL 中不考虑计数窗口,所以滚动窗口就是滚动时间窗口,参数中还需要将当前的时间属性字段传入;

​ 另外,窗口 TVF 本质上是表函数,可以对表进行扩展,所以还应该把当前查询的表作为参数整体传入。具体声明如下:

TUMBLE(TABLE table1, DESCRIPTOR(ts), INTERVAL '1' HOUR) 
    参数:(表名, 时间属性字段, 滚动长度)
    这里基于时间字段 ts,对表 table1 中的数据开了大小为 1 小时的滚动窗口。窗口会将表中的每一行数据,按照它们 ts 的值分配到一个指定的窗口中。 
7.4.3.3 滑动窗口Hop
HOP(TABLE table1, DESCRIPTOR(ts), INTERVAL '5' MINUTES, INTERVAL '1' HOURS);
    参数:(表名, 时间属性字段, 滑动步长slide, 窗口大小size) 
    这里我们基于时间属性 ts,在表 EventTable 上创建了大小为 1 小时的滑动窗口,每 5 分钟滑动一次。
7.4.3.4 累计窗口Cumulate

累积窗口是什么?

滚动窗口和滑动窗口,可以用来计算大多数周期性的统计指标。不过在实际应用中还会遇到这样一类需求:我们的统计周期可能较长,因此希望中间每隔一段时间就输出一次当前的统计值;与其他窗口不同的是,累积窗口在一个统计周期内,我们会多次输出统计值,它们应该是不断叠加累积的。

例如,我们按天来统计网站的 PV(Page View,页面浏览量),如果用 1 天的滚动窗口,那需要到每天 24 点才会计算一次,输出频率太低;如果用滑动窗口,计算频率可以更高,但统计的就变成了“过去 24 小时的 PV”。所以我们真正希望的是,还是按照自然日统计每天的 PV,不过需要每隔 1 小时就输出一次当天到目前为止的 PV 值。这种特殊的窗口就叫作“累积窗口”(Cumulate Window)。

CUMULATE(TABLE table1, DESCRIPTOR(ts), INTERVAL '1' HOURS, INTERVAL '1' DAYS))
    参数:(表名, 时间属性字段, 步长, 最大窗口长度) 
    基于时间属性 ts,在表 table1 上定义了一个统计周期为 1 天、累积步长为 1 小时的累积窗口。注意第三个参数为步长 step,第四个参数则是最大窗口长度。 

7.4.4 聚合 Aggregation

  • TimeAndWindowTest
7.4.4.1 分组聚合
SUM()、MAX()、MIN()、AVG() 以及 COUNT()
比如简单写:
Table eventCountTable = tableEnv.sqlQuery("select COUNT(*) from EventTable"); 

而更多的情况下,应通过 GROUP BY 子句来指定分组的键(key),从而对数据按照某个字段做一个分组统计。
SELECT user, COUNT(url) as cnt FROM EventTable GROUP BY user 
                                                              GROUP BY 相当于 datastream里的 key by

在流处理中,分组聚合同样是一个持续查询,而且是一个更新查询,得到的是一个动态表;

每当流中有一个新的数据到来时,都会导致结果表的更新操作。

因此,想要将结果表转换成流或输出到外部系统,必须采用撤回流(retract stream)或更新插入流(upsert stream)的编码方式;

如果在代码中直接转换成 DataStream 打印输出,需要调用 toChangelogStream()。

另外,在持续查询的过程中,由于用于分组的 key 可能会不断增加,因此计算结果所需要维护的状态也会持续增长。

为了防止状态无限增长耗尽资源,Flink Table API 和 SQL 可以在表环境中配置状态的生存时间(TTL):

TableEnvironment tableEnv = ... 
// 获取表环境的配置 
TableConfig tableConfig = tableEnv.getConfig(); 
// 配置状态保持时间 
tableConfig.setIdleStateRetention(Duration.ofMinutes(60)); 



或者也可以直接设置配置项 table.exec.state.ttl: 
TableEnvironment tableEnv = ... 
Configuration configuration = tableEnv.getConfig().getConfiguration(); configuration.setString("table.exec.state.ttl", "60 min"); 
7.4.4.2 窗口聚合
  • TimeAndWindowTest

在流处理中,往往需要将无限数据流划分成有界数据集,这就是所谓的“窗口”。

窗口分类如7.4.3所示

// 3. 窗口聚合
// 3.1 滚动窗口
Table tumbleWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " +
        " window_end AS endT " +
        "FROM TABLE( " +
        "  TUMBLE( TABLE clickTable, DESCRIPTOR(et), INTERVAL '10' SECOND)" +
        ") " +
        "GROUP BY user_name, window_start, window_end "
);
tableEnv.toDataStream(tumbleWindowResultTable).print("tumble window: ");



// 3.2 滑动窗口
Table hopWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " +
        " window_end AS endT " +
        "FROM TABLE( " +
        "  HOP( TABLE clickTable, DESCRIPTOR(et), INTERVAL '5' SECOND, INTERVAL '10' SECOND)" +
        ") " +
        "GROUP BY user_name, window_start, window_end "
);

// 3.3 累积窗口
Table cumulateWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " +
        " window_end AS endT " +
        "FROM TABLE( " +
        "  CUMULATE( TABLE clickTable, DESCRIPTOR(et), INTERVAL '5' SECOND, INTERVAL '10' SECOND)" +
        ") " +
        "GROUP BY user_name, window_start, window_end "
);
7.4.3.3 开窗聚合Over
  • TimeAndWindowTest

在标准 SQL 中还有另外一类比较特殊的聚合方式,可以针对每一行计算一个聚合值。

比如说,我们可以以每一行数据为基准,计算它之前 1 小时内所有数据的平均值;也可以计算它之前 10 个数的平均值。

就好像是在每一行上打开了一扇窗户、收集数据进行统计一样,这就是所谓的“开窗函数”。

开窗函数是通过 OVER 子句来实现的,所以有时开窗聚合也叫作“OVER 聚合”(Over Aggregation)。基本语法如下:

SELECT 
  <聚合函数> OVER ( 
    [PARTITION BY <字段1>[, <字段2>, ...]] 
    ORDER BY <时间属性字段> 
    <开窗范围>), 
  ... 
FROM ... 
  • PARTITION BY:用来指定分区的键(key),类似于 GROUP BY 的分组,这部分是可选的;

  • ORDER BY :OVER 窗口是基于当前行扩展出的一段数据范围,选择的标准可以基于时间也可以基于数量。不论那种定义,数据都应该是以某种顺序排列好的;而表中的数据本身是无序的。所以在 OVER 子句中必须用 ORDER BY 明确地指出数据基于那个字段排序。在 Flink 的流处理中,目前只支持按照时间属性的升序排列,所以这里 ORDER BY 后面的字段必须是定义好的时间属性。

  • 开窗范围:开窗选择的范围可以基于时间,也可以基于数据的数量。所以开窗范围还应该在两种模式之间做出选择:范围间隔 / 行间隔。

    • 范围间隔:一般就是当前行时间戳之前的一段时间。例如开窗范围选择当前行之前 1 小时的数据:

    • RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW 
      
    • 行间隔:确定要选多少行,由当前行出发向前选取就可以了。例如开窗范围选择当前行之前的 5 行数据(最终聚合会包括当前行,所以一共 6 条数据)

    • ROWS BETWEEN 5 PRECEDING AND CURRENT ROW 
      
  • 示例:

// over聚合   每个用户当前这次访问,和之前三次访问的平均时间戳avg
Table overWindowResultTable = tableEnv.sqlQuery("SELECT user_name, " +
        " avg(ts) OVER (" +
        "   PARTITION BY user_name " +
        "   ORDER BY et " +
        "   ROWS BETWEEN 3 PRECEDING AND CURRENT ROW" +
        ") AS avg_ts " +
        "FROM clickTable");
tableEnv.toDataStream(overWindowResultTable).print("over window: ");

上面示例只求一个聚合(平均时间戳avg),如果要多个聚合呢?

两个聚合结果COUNT(url)、 MAX(CHAR_LENGTH(url))
SELECT user, ts, 
  COUNT(url) OVER w AS cnt, 
  MAX(CHAR_LENGTH(url)) OVER w AS max_url 
FROM EventTable 
WINDOW w AS ( 
  PARTITION BY user 
  ORDER BY ts 
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) 
-------定义了一个 w ,WINDOW w AS ( ) 里面写over窗口的语法
7.4.3.4 应用实例 —— Top N

灵活使用各种类型的窗口以及聚合函数,可以实现不同的需求。一般的聚合函数,比如 SUM()、MAX()、MIN()、COUNT()等,往往只是针对一组数据聚合得到一个唯一的值;所谓 OVER 聚合的“多对多”模式,也是针对每行数据都进行一次聚合才得到了多行的结果,对于每次聚合计算实际上得到的还是唯一的值。而有时我们可能不仅仅需要统计数据中的最大/最小值,还希望得到前 N 个最大/最小值;这时每次聚合的结果就不是一行,而是 N 行了。这就是经典的“Top N”应用场景。

  • 第一类 普通 Top N

没有窗口操作,只是简单聚合。是通过 OVER 聚合和一个条件筛选来实现 Top N 的。

基本语法:

SELECT ... 
FROM ( 
   SELECT ..., 
      ROW_NUMBER() OVER ( 
      [PARTITION BY <字段1>[, <字段1>...]] 
          ORDER BY <排序字段1> [asc|desc][, <排序字段2> [asc|desc]...] 
) AS row_num 
   FROM ...) 
WHERE row_num <= N [AND <其它条件>] 
----------------------------
筛选出行号小于n的
通过将一个特殊的聚合函数ROW_NUMBER()应用到OVER窗口上,统计出每一行排序后的行号,作为一个字段row_num提取出来;
然后再用 WHERE 子句筛选行号小于等于 N 的那些行返回。 
  • WHERE:用来指定 Top N 选取的条件,这里必须通过 row_num <= N 或者 row_num < N + 1 指定一个“排名结束点”(rank end),以保证结果有界。

  • PARTITION BY :是可选的,用来指定分区的字段,这样我们就可以针对不同的分组分别统计 Top N 了。

  • ORDER BY :指定排序的字段,因为只有排序之后,才能进行前 N 个最大/最小的选取。每个排序字段后可以用 asc 或者 desc 来指定排序规则:asc 为升序排列,取出的就是最小的 N 个值;desc 为降序排序,对应的就是最大的 N 个值。默认情况下为升序,asc 可以省略。

示例1:

SELECT user, url, ts, row_num 
FROM ( 
   SELECT *, 
      ROW_NUMBER() OVER ( 
	  PARTITION BY user 
          ORDER BY CHAR_LENGTH(url) desc  
) AS row_num 
   FROM EventTable) 
WHERE row_num <= 2 
-----------------------------
这里我们以用户来分组,以访问 url 的字符长度作为排序的字段,降序排列后用聚合统计出每一行的行号,这样就相当于在 EventTable 基础上扩展出了一列 row_num。
而后筛选出行号小于等于 2 的所有数据,就得到了每个用户访问的长度最长的两个 url。 

示例2:

  • TopNExample
Table topNResultTable = tableEnv.sqlQuery("SELECT user, cnt, row_num " +                // 1 查询出这些信息
        "FROM (" +                                                                     //     3 从这里查询
        "   SELECT *, ROW_NUMBER() OVER (" +       // 5 ROW_NUMBER应用到OVER窗口上,统计出每一行排序后的行号,作为字段row_num提取出
        "      ORDER BY cnt DESC" +                                                     //     4 按照cnt降序
        "   ) AS row_num " +                                            //
        "   FROM (SELECT user, COUNT(url) AS cnt FROM clickTable GROUP BY user)" +      //      5 作为row_num
        ") WHERE row_num <= 2");                                                        // 2 挑选出row_num小于2的
tableEnv.toChangelogStream(topNResultTable).print("top 2: ");        
-------------------------
选取当前所有用户中浏览量最大的2个
注:这里的from没有跟一个表,而是跟一个查询语句
	why?
	因为表中字段没有cnt,所以用select先计算出cnt,才能够实现前面的语句
	
  • 第二类 窗口 Top N

除了直接对数据进行 Top N 的选取,我们也可以针对窗口来做 Top N。

例如电商行业,实际应用中往往有这样的需求:统计一段时间内的热门商品。这就需要先开窗口,在窗口中统计每个商品的点击量;然后将统计数据收集起来,按窗口进行分组,并按点击量大小降序排序,选取前 N 个作为结果返回。

146

7.4.5 联结查询 join

147

例如商品的订单信息,我们会保存在一个 “订单表”中,而这个表中只有商品 ID,详情则需要到“商品表”按照 ID 去查询;这样的好处是当商品信息发生变化时,只要更新商品表即可,而不需要在订单表中对所有这个商品的所有订单进行修改。不过这样一来,我们就无法从一个单独的表中提取所有想要的数据了。在标准 SQL 中,可以将多个表连接合并起来,从中查询出想要的信息;这种操作就是表的联结(Join)。在 Flink SQL 中,同样支持各种灵活的联结(Join)查询,操作的对象是动态表。

在流处理中,动态表的 Join 对应着两条数据流的 Join 操作。与上一节的聚合查询类似, Flink SQL 中的联结查询大体上也可以分为两类:SQL 原生的联结查询方式,和流处理中特有的联结查询。

7.4.5.1 常规联结查询

常规联结(Regular Join)是 SQL 中原生定义的 Join 方式,是最通用的一类联结操作。它的具体语法与标准 SQL 的联结完全相同,通过关键字 JOIN 来联结两个表,后面用关键字 ON 来指明联结条件。按照习惯,我们一般以“左侧”和“右侧”来区分联结操作的两个表。

在两个动态表的联结中,任何一侧表的插入(INSERT)或更改(UPDATE)操作都会让联结的结果表发生改变。例如,如果左侧有新数据到来,那么它会与右侧表中所有之前的数据进行联结合并,右侧表之后到来的新数据也会与这条数据连接合并。所以,常规联结查询一般是更新(Update)查询。

与标准 SQL 一致,Flink SQL 的常规联结也可以分为内联结(INNER JOIN)和外联结(OUTER JOIN),区别在于结果中是否包含不符合联结条件的行。目前仅支持“等值条件” 作为联结条件,也就是关键字 ON 后面必须是判断两表中字段相等的逻辑表达式。

7.4.5.2 间隔联结查询

7.4.6 函数

在 SQL 中,我们可以把一些数据的转换操作包装起来,嵌入到 SQL 查询中统一调用,这就是“函数”(functions)。

Flink 的 Table API 和 SQL 同样提供了函数的功能。两者在调用时略有不同:

1 Table API 中的函数是通过数据对象的方法调用来实现的;

2 而 SQL 则是直接引用函数名称,传入数据作为参数。

例如,要把一个字符串 str 转换成全大写的形式:

1 Table API 的写法是调用 str 这个 String 对象的 upperCase()方法:

str.upperCase();

2 而 SQL 中的写法就是直接引用 UPPER()函数,将 str 作为参数传入:

UPPER(str)

由于 Table API 是内嵌在 Java 语言中的,很多方法需要在类中额外添加,因此扩展功能比较麻烦,目前支持的函数比较少;而且 Table API 也不如 SQL 的通用性强,所以一般情况下较少使用。下面我们主要介绍 Flink SQL 中函数的使用。

Flink SQL 中的函数可以分为两类:

  • 一类是 SQL 中内置的系统函数:直接通过函数名调用就可以,能够实现一些常用的转换操作,比如之前我们用到的 COUNT()、CHAR_LENGTH()、 UPPER()等等;

  • 另一类函数则是用户自定义的函数(UDF):需要在表环境中注册才能使用。

7.4.6.1 系统函数

系统函数(System Functions)也叫内置函数(Built-in Functions),是在系统中预先实现好的功能模块。

我们可以通过固定的函数名直接调用,实现想要的转换操作。Flink SQL 提供了大量的系统函数,几乎支持所有的标准 SQL 中的操作,这为我们使用 SQL 编写流处理程序提供了极大的方便。

Flink SQL 中的系统函数又主要可以分为两大类:标量函数(Scalar Functions)和聚合函数(Aggregate Functions)。

  • 1 标量函数

所谓的“标量”,是指只有数值大小、没有方向的量;所以标量函数指的就是只对输入数据做转换操作、返回一个值的函数。这里的输入数据对应在表中,一般就是一行数据中 1 个或多个字段,因此这种操作有点像流处理转换算子中的 map。另外,对于一些没有输入参数、直接可以得到唯一结果的函数,也属于标量函数。

标量函数是最常见、也最简单的一类系统函数,数量非常庞大,这里做一个简单概述,具体应用可以查看官网的完整函数列表

比较函数

比较函数其实就是一个比较表达式,用来判断两个值之间的关系,返回一个布尔类型的值。这个比较表达式可以是用 <、>、= 等符号连接两个值,也可以是用关键字定义的某种判断。

例如:

(1)value1 = value2 判断两个值相等;

(2)value1 <> value2 判断两个值不相等

(3)value IS NOT NULL 判断 value 不为空

逻辑函数

逻辑函数就是一个逻辑表达式,也就是用与(AND)、或(OR)、非(NOT)将布尔类型的值连接起来,也可以用判断语句(IS、IS NOT)进行真值判断;返回的还是一个布尔类型的值。例如:

(1)boolean1 OR boolean2 布尔值 boolean1 与布尔值 boolean2 取逻辑或

(2)boolean IS FALSE 判断布尔值 boolean 是否为 false

(3)NOT boolean 布尔值 boolean 取逻辑非

算术函数

进行算术计算的函数,包括用算术符号连接的运算,和复杂的数学运算。例如:

(1)numeric1 + numeric2 两数相加

(2)POWER(numeric1, numeric2) 幂运算,取数 numeric1 的 numeric2 次方(3)RAND() 返回(0.0, 1.0)区间内的一个 double 类型的伪随机数

字符串函数

进行字符串处理的函数。例如:

(1)string1 || string2 两个字符串的连接

(2)UPPER(string) 将字符串 string 转为全部大写

(3)CHAR_LENGTH(string) 计算字符串 string 的长度

时间函数

进行与时间相关操作的函数。例如:

(1) DATE string 按格式"yyyy-MM-dd"解析字符串 string,返回类型为 SQL Date

(2) TIMESTAMP string 按格式"yyyy-MM-dd HH:mm:ss[.SSS]"解析,返回类型为 SQL timestamp

(3) CURRENT_TIME 返回本地时区的当前时间,类型为 SQL time(与 LOCALTIME 等价)

(4) INTERVAL string range 返回一个时间间隔。string 表示数值;range 可以是 DAY,

MINUTE,DAT TO HOUR 等单位,也可以是 YEAR TO MONTH 这样的复合单位。如“2 年

10 个月”可以写成:INTERVAL ‘2-10’ YEAR TO MONTH

  • 2 聚合函数

与标量函数不同的是,聚合函数是把多行值进行输入,聚合输出

标准 SQL 中常见的聚合函数 Flink SQL 都是支持的,目前也在不断扩展,为流处理应用提供更强大的功能。例如:

⚫ COUNT(*) 返回所有行的数量,统计个数

⚫ SUM([ ALL | DISTINCT ] expression) 对某个字段进行求和操作。默认情况下省略了关键字 ALL,表示对所有行求和;如果指定 DISTINCT,则会对数据进行去重,每个值只叠加一次。

⚫ RANK() 返回当前值在一组值中的排名

⚫ ROW_NUMBER() 对一组值排序后,返回当前值的行号。与 RANK()的功能相似

其中,RANK()和 ROW_NUMBER()一般用在 OVER 窗口中,在之前实现 Top N 的过程中起到了非常重要的作用。

7.4.6.2 自定义函数(UDF)

149

系统函数尽管庞大,也不可能涵盖所有的功能;如果有系统函数不支持的需求,我们就需要用自定义函数(User Defined Functions,UDF)来实现了。事实上,系统内置函数仍然在不断扩充,如果我们认为自己实现的自定义函数足够通用、应用非常广泛,也可以在项目跟踪工具 JIRA 上向 Flink 开发团队提出“议题”(issue),请求将新的函数添加到系统函数中。

Flink 的 Table API 和 SQL 提供了多种自定义函数的接口,以抽象类的形式定义。当前 UDF 主要有以下几类:

  • 标量函数(Scalar Functions):将输入的标量值转换成一个新的标量值;

  • 表函数(Table Functions):将标量值转换成一个或多个新的行数据,也就是扩展成一个表;

  • 聚合函数(Aggregate Functions):将多行数据里的标量值转换成一个新的标量值;

  • 表聚合函数(Table Aggregate Functions):将多行数据里的标量值转换成一个或多个新的行数据。

整体调用流程

  • 注册函数 :注册函数时需要调用表环境的 createTemporarySystemFunction()方法,传入注册的函数名以及 UDF 类的 Class 对象:
// 注册函数 
tableEnv.createTemporarySystemFunction("MyFunction", MyFunction.class); 
-------------------我们自定义的 UDF 类叫作 MyFunction,在环境中将它注册为名叫 MyFunction 的函数。 
  • 在 SQL 中调用函数

当我们将函数注册为系统函数之后,在 SQL 中的调用就与内置系统函数完全一样了:

tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable"); 
-----------------------------调用MyFunction方法,把myField传入

自定义函数接口如何实现:

150

  • 标量函数

  • 表函数

  • 聚合函数

  • 表聚合函数

7.4.7 SQL 客户端

有了 Table API 和 SQL,我们就可以使用熟悉的 SQL 来编写查询语句进行流处理了。不过,这种方式还是将 SQL 语句嵌入到 Java/Scala 代码中进行的;另外,写完的代码后想要提交作业还需要使用工具进行打包。这都给 Flink 的使用设置了门槛,如果不是 Java/Scala 程序员,即使是非常熟悉 SQL 的工程师恐怕也会望而生畏了。

基于这样的考虑,Flink 为我们提供了一个工具来进行 Flink 程序的编写、测试和提交,这工具叫作“SQL 客户端”。SQL 客户端提供了一个命令行交互界面(CLI),我们可以在里面非常容易地编写 SQL 进行查询,就像使用 MySQL 一样;整个 Flink 应用编写、提交的过程全变成了写 SQL,不需要写一行 Java/Scala 代码。

启动本地集群

./bin/start-cluster.sh

启动 Flink SQL 客户端

./bin/sql-client.sh 

设置运行模式

首先是表环境的运行时模式,有流处理和批处理两个选项。默认为流处理:

Flink SQL> SET 'execution.runtime-mode' = 'streaming';

其次是 SQL 客户端的“执行结果模式”,主要有 table、changelog、tableau 三种,默认为 table 模式:

Flink SQL> SET 'sql-client.execution.result-mode' = 'table'; 
-----------table 模式就是最普通的表处理模式,结果会以逗号分隔每个字段;
			changelog 则是更新日志模式,会在数据前加上“+”(表示插入)或“-”(表示撤回)的前缀;
			而 tableau 则是经典的可视化表模式,结果会是一个虚线框的表格。 

此外我们还可以做一些其它可选的设置,比如之前提到的空闲状态生存时间(TTL):

Flink SQL> SET 'table.exec.state.ttl' = '1000'; 

执行 SQL 查询

Flink SQL> CREATE TABLE EventTable( 
>   user STRING, 
>   url STRING, 
>   `timestamp` BIGINT 
> ) WITH ( 
>   'connector' = 'filesystem', 
>   'path'      = 'events.csv', 
>   'format'    = 'csv' 
> ); 
 
Flink SQL> CREATE TABLE ResultTable ( 
>   user STRING, 
>   cnt BIGINT 
> ) WITH ( 
>   'connector' = 'print'                                 
> ); 

-------------------------------------------  'connector' = 'print' 在控制台输出
 
Flink SQL> INSERT INTO ResultTable SELECT user, COUNT(url) as cnt FROM EventTable 
 GROUP BY user; 

7.5 连接到外部系统

7.5.1 控制台打印

在表环境(TableEnvironment)下,如何把数据打印在控制台

//3.1 再创建一个用于控制台打印的表
String creatPrintOutDDl = "CREATE TABLE printoutTable(" +
        "url STRING," +
        "user_name STRING" +
        ")WITH (" +
        " 'connector' =  'print' " +
        ")";
tableEnv.executeSql(creatPrintOutDDl);       //执行sql语句creatPrintOutDDl
image-20220326154353286

7.5.2 csv

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-csv</artifactId> 
  <version>${flink.version}</version> 
</dependency> 

Flink 为各种连接器提供了一系列的“表格式”(table formats),比如 CSV、JSON、 Avro、Parquet 等等。SQL 客户端中已经内置了

CSV、JSON 的支持,因此使用时无需专门引入;

7.6 断点续传

<!--如果保存检查点到 hdfs 上,需要引入此依赖-->
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>${hadoop.version}</version>
</dependency>

首先知道检查点、保存点位置在哪里指定:

  • jar包中,设置了checkpoints地址
        //2.Flink-CDC 将读取 binlog 的位置信息以状态的方式保存在 CK,如果想要做到断点续传,需要从 Checkpoint 或者 Savepoint 启动程序
        env.enableCheckpointing(5000L);                         //2.1 开启 Checkpoint,每隔 5 秒钟做一次 CK
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);          //2.2 指定 CK 的一致性语义
        env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);      //2.3 设置任务关闭的时候保留最后一次 CK 数据
                     //env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 2000L));            //2.4 指定 CK 自动重启策略   老版本1.10版本需要设置
        env.setStateBackend(new FsStateBackend("hdfs://hadoop102:9000/gmall/checkpoints"));               //2.5 设置状态后端为FS
//        System.setProperty("HADOOP_USER_NAME", "dwc");                    //2.6 设置访问 HDFS 的用户名
  • 手动做保存点时,设置了savepoint位置

步骤:

提交一个job

./bin/flink run -m hadoop102:8081 -c com.dwc.FlinkCDC ./job/gmall-flink-cdc-1.0-SNAPSHOT-jar-with-dependencies.jar

web进入Task Managers , 可以看到103少一个slot,

image-20220428215307677

进入 查看是否允许成功并print,运行成功

image-20220428215356695

取消任务之前,正常cancel任务要先保存点,(后面为保存点路径)设置了savepoint位置

bin/flink savepoint 143930f412ae147f1657afa3604a327d hdfs://hadoop102:9000/gmall/savepoint

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YmSosNua-1653008673143)(flink笔记.assets/image-20220428220746047.png)]

然后取消任务

从保存点重新启动job

./bin/flink run -m hadoop102:8081 -s hdfs://hadoop102:9000/gmall/savepoint/savepoint-143930-9265000a6738 -c com.dwc.FlinkCDC ./job/gmall-flink-cdc-1.0-SNAPSHOT-jar-with-dependencies.jar

查看web,依然是103少一个slot

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5eGrvBq-1653008673144)(flink笔记.assets/image-20220428221113373.png)]

断点续传成功

7.7 DataStream\SQL对比

  • DataStream

    优点:多库多表同步

    缺点:需要自定义反序列化器

  • Flink SQL

    优点:不需要自定义反序列化器

    缺点:单表同步

8 Flink CEP

在实际应用中,还有一类需求是要检测以特定顺序先后发生的一组事件,进行统计或做报警提示,这就比较麻烦了。例如,网站做用户管理,可能需要检测“连续登录失败”事件的发生,这是个组合事件,其实就是“登录失败”和“登录失败”的组合;电商网站可能需要检测用户“下单支付”行为,这也是组合事件,“下单”事件之后一段时间内又会有“支付”事件到来,还包括了时间上的限制。

类似的多个事件的组合,我们把它叫作“复杂事件”。对于复杂时间的处理,由于涉及到事件的严格顺序,有时还有时间约束,我们很难直接用 SQL 或者 DataStream API 来完成。于是只好放大招——派底层的处理函数(process function)上阵了。处理函数确实可以搞定这些需求,不过对于非常复杂的组合事件,我们可能需要设置很多状态、定时器,并在代码中定义各种条件分支(if-else)逻辑来处理,复杂度会非常高,很可能会使代码失去可读性。怎样处理这类复杂事件呢?Flink 为我们提供了专门用于处理复杂事件的库——CEP,可以让我们更加轻松地解决这类棘手的问题。这在企业的实时风险控制中有非常重要的作用。

本章我们就来了解一下 Flink CEP 的用法。

8.1 CEP 是什么

所谓 CEP,其实就是“复杂事件处理(Complex Event Processing)”的缩写;而 Flink CEP,就是 Flink 实现的一个用于复杂事件处理的库(library)。

那到底什么是“复杂事件处理”呢?就是可以在事件流里,检测到特定的事件组合并进行处理,比如说“连续登录失败”,或者“订单支付超时”等等。

具体的处理过程是,把事件流中的一个个简单事件,通过一定的规则匹配组合起来,这就是“复杂事件”;然后基于这些满足规则的一组组复杂事件进行转换处理,得到想要的结果进行输出。

总结起来,复杂事件处理(CEP)的流程可以分成三个步骤:

(1)定义一个匹配规则

(2)将匹配规则应用到事件流上,检测满足规则的复杂事件

(3)对检测到的复杂事件进行处理,得到结果进行输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jrj0VAt6-1653008673144)(flink笔记.assets/image-20220424092142184.png)]

如图 12-1 所示,输入是不同形状的事件流,我们可以定义一个匹配规则:在圆形后面紧跟着三角形。那么将这个规则应用到输入流上,就可以检测到三组匹配的复杂事件。它们构成了一个新的“复杂事件流”,流中的数据就变成了一组一组的复杂事件,每个数据都包含了一个圆形和一个三角形。接下来,我们就可以针对检测到的复杂事件,处理之后输出一个提示或报警信息了。

所以,CEP 是针对流处理而言的,分析的是低延迟、频繁产生的事件流。它的主要目的,就是在无界流中检测出特定的数据组合,让我们有机会掌握数据中重要的高阶特征。

9 tips

9.1 keyBy

关系到自定义全窗口函数的参数
.keyBy("url")   //url是Tuple类型
.keyBy(ApacheLogEvent::getUrl) // key是String类型  
.keyBy(data -> data.f0)        //f0:二元组位置0 作为key    (lambda表达式写法  此时key是原本的Integer类型) 

------------------------------------------------------------------------------

0 基于Flink的电商用户行为数据分析 - EcommerceAnaly

1 介绍

1.1 基于这些数据能做什么?

1.2 项目模块设计

1.3 数据源

  • CSV文件

    注:时间戳一般10位是秒,13位是毫秒

  • web服务器日志

1.4 项目模块

  • 实时热门商品统计
  • 实时流量统计
  • 市场营销分析
  • 恶意登录监控
  • 订单支付实时监控

2 思路

2.1 热门实时商品统计

  • 基本需求

– 统计近1小时内的热门商品,每5分钟更新一次

– 热门度用浏览次数(“pv”)来衡量

  • 解决思路

– 在所有用户行为数据中,过滤出浏览(“pv”)行为进行统计

– 构建滑动窗口,窗口长度为1小时,滑动距离为5分钟

**–窗口聚合:**将原本的数据类型要变为itemViewCount;

如果用reduce,输出类型无法改变,所以使用aggregate;

聚合最后全部要输出的数据为:itemViewCount(包含:商品id,窗口结束时间,计数数量count)

左边new方法拿到count,右边new方法拿到窗口信息

只有aggregate的话,输出拿不到windowEnd的信息,所以使用:两个new -> 增量聚合+全窗口的调用方式

左边是增量聚合,右边是全窗口,此时的全窗口不用拿全部数据,只拿前面聚合的结果,包装一下输出结果

**–增量聚合:**实现聚合策略(实现必须写的四个方法),作用:拿到count值,来一个+1

实现 AggregateFunction 接口 : interface AggregateFunction<IN, ACC, OUT>

–全窗口函数WindowFunction<>: 定义新的输出类型itemViewCount,实现 WindowFunction 接口,要拿到itemId、windowEnd、count数据

– interface WindowFunction <IN, OUT, KEY, W extends Window>

• IN: 输入为累加器的类型,Long

• OUT: 窗口累加以后输出的类型为 ItemViewCount(itemId: Long, windowEnd: Long, count: Long), windowEnd为窗口的 结束时间,也是窗口的唯一标识

• KEY: Tuple泛型,在这里是 itemId,窗口根据itemId聚合 ,

注:Tuple(二元组),keyby是啥它就是啥,keyby只有一个itemId,所以Tuple只有一个itemId,因此取Long itemId = tuple.getField(0)

• W: 聚合的窗口,w.getEnd 就能拿到窗口的结束时间

–函数ProcessWindowFunction:在AppMarketingByChannel用到这个函数,它可以拿到上下文,因此做示例

上面聚合输出的结果是没有区分的,但目的是要收集某一时间段的热门商品,所以要以 windowEnd 作为 key,保证都是同一个窗口内的,然后收集同一窗口的所有商品count数据,排序输出top

怎么排序?

实现,要考虑必须让数据攒齐,再进行排序

  • 最终排序输出 —— keyedProcessFunction

– 针对有状态流的底层API – KeyedProcessFunction 会对分区后的每一条子流进行处理

– 以 windowEnd 作为 key,保证排序的都是同一个窗口内的

– 从 ListState 中读取当前流的状态,存储数据进行排序输出

  • 用 ProcessFunction 来定义 KeyedStream 的处理逻辑

  • 分区之后,每个 KeyedStream 都有其自己的生命周期

– open:初始化,在这里可以获取当前流的状态

– processElement:处理流中每一个元素时调用

– onTimer:定时调用,注册定时器 Timer 并触发之后的回调操作

2.2 实时流量统计–热门页面

  • 基本需求

– 从web服务器的日志中,统计实时的热门访问页面

– 统计每分钟的ip访问量,取出访问量最大的5个地址,每5秒更新一次

  • 解决思路

– 将 apache 服务器日志中的时间,转换为时间戳,作为 Event Time

– 构建滑动窗口,窗口长度为1分钟,滑动距离为5秒

2.3 实时流量统计 —— PV 和 UV

  • 基本需求

– 从埋点日志中,统计实时的 PV 和 UV

– 统计每小时的访问量(PV),并且对用户进行去重(UV)

  • 解决思路

– 统计埋点日志中的 pv 行为,利用 Set 数据结构进行去重

– 对于超大规模的数据,可以考虑用布隆过滤器进行去重

2.4 市场营销分析 —— APP 市场推广统计

  • 基本需求

– 从埋点日志中,统计 APP 市场推广的数据指标

– 按照不同的推广渠道,分别统计数据

  • 解决思路

– 通过过滤日志中的用户行为,按照不同的渠道进行统计

– 可以用 process function 处理,得到自定义的输出数据信息

2.5 市场营销分析 —— 页面广告统计

  • 基本需求

– 从埋点日志中,统计每小时页面广告的点击量,5秒刷新一次,并按照不同省 份进行划分

– 对于“刷单”式的频繁点击行为进行过滤,并将该用户加入黑名单

  • 解决思路

– 根据省份进行分组,创建长度为1小时、滑动距离为5秒的时间窗口进行统计

– 可以用 process function 进行黑名单过滤,检测用户对同一广告的点击量, 如果超过上限则将用户信息以侧输出流输出到黑名单中

2.6 恶意登录监控

  • 基本需求

– 用户在短时间内频繁登录失败,有程序恶意攻击的可能

– 同一用户(可以是不同IP)在2秒内连续两次登录失败,需要报警

  • 解决思路

– 将用户的登录失败行为存入 ListState,设定定时器2秒后触发,查看 ListState 中有几次失败登录

– 更加精确的检测,可以使用 CEP 库实现事件流的模式匹配

2.7 订单支付实时监控

  • 基本需求

– 用户下单之后,应设置订单失效时间,以提高用户支付的意愿,并降 低系统风险

– 用户下单后15分钟未支付,则输出监控信息

  • 解决思路

– 利用 CEP 库进行事件流的模式匹配,并设定匹配的时间间隔

– 也可以利用状态编程,用 process function 实现处理逻辑

2.8 订单支付实时对账

  • 基本需求

– 用户下单并支付后,应查询到账信息,进行实时对账

– 如果有不匹配的支付信息或者到账信息,输出提示信息

  • 解决思路

– 从两条流中分别读取订单支付信息和到账信息,合并处理

– 用 connect 连接合并两条流,用 coProcessFunction 做匹配处理

3 实时热门商品统计 Hotitems

放在父pom文件的依赖:flink、kafka

对版本进行声明,后续要改版本,也只需从这里修改
<properties>
    <flink.version>1.10.1</flink.version>
    <scala.binary.version>2.12</scala.binary.version>
    <kafka.version>2.2.0</kafka.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-java</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_${scala.binary.version}</artifactId>
        <version>${kafka.version}</version>
    </dependency>
    <dependency>
        <!--   flink连接kafka     -->
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
              <!--   编译的插件  如果已经设置可以不用写   -->
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

Module–HotItemsAnalysis

  • resources–UserBehavior.csv 有48万条数据 ,使用KafkaProducerUtil读取它
  • UserBehavior实体类:CSV文件数据类型
  • itemViewCount实体类:在两个new的窗口聚合时,产生的itemViewCount,需要新的实体类
  • Hotitems分析类:接收topic=hotitems
  • KafkaProducerUtil:kafka数据脚本,自动读取csv文件,通过kafka传输给分析类
  • HotItemsWithSql:用sql实现同样的需求(=Hotitems分析类)
--增量函数AggregateFunction<in,聚合,out>

--全窗口函数WindowFunction<in,out,k,w>
public void apply(Tuple tuple, TimeWindow timeWindow, Iterable<Long> iterable, Collector<ItemViewCount> collector
(1) Long itemId = tuple.getField(0)     //当前key就是按照itemId分的组,key只有一个itemId,Tuple索引0为itemId
(2) TimeWindow信息
(3) 前面增量聚合输出的数据都在Iterable里
(4) collector.collect()输出

--实现自定义extends keyedProcessFunction<key,in,out>
topSize 定义属性 top n 的大小
定义listState  保存当前窗口内所有输出的ItemViewCount
open:具体定义listState 
processElement:每来一条数据都会调用processElement这个方法
onTimer:后定时器触发,当前已收集到所有数据,排序输出
image-20220301145953559

测试1

kafka生产者
./bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic hotitems

kafka消费者
./bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic

启动Hotitems、生产者

第1步第一条数据进入,第2步已经过300s,但是计时器+1,所以在第三步301时触发计算

image-20220211172250337

测试2

kafka批量数据测试

开启Hotitems,再开启KafkaProducerUtil

Flink SQL实现需求

  • HotItemsWithSql
环境依赖1.10.1版本,加了blink,若flink-1.11后的planner建议用新版本planner
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner-blink_2.12</artifactId>
    <version>1.10.1</version>
</dependency>

3 实时流量统计

​ 现在要实现的模块是 “实时流量统计”。对于一个电商平台而言,用户登录的入口流量、不同页面的访问流量都是值得分析的重要数据,而这些数据,可以简单地从 web 服务器的日志中提取出来。

3.1 热门页面排行统计 HotPages⭐

统计出点击量高的网站,进行排行

过滤出get请求

乱序 三重保证

  • Module - NetworkFlowAnalysis
  • resources - apache.log web日志文件
  • ApacheLogEvent实体类:.log文件数据
  • PageViewCount实体类:在两个new的窗口聚合时,新的PageViewCount,需要新的实体类,此实体类为了输出信息含有 url、windowEnd、count
  • HotPages分析类: String -> ApacheLogEvent -> PageViewCount
---把文件里的17/05/2015:10:05:03时间转为时间戳
new SimpleDateFormat("dd/MM/yyyy:HH:mm:ss");
.parse()
乱序数据:
第一重保证:引入watermark
第二重保证:允许迟到数据
.allowedLateness(Time.minutes(1))
第三重保证:侧输出流
 OutputTag<ApacheLogEvent> lateTag = new OutputTag<ApacheLogEvent>("late"){};   //事先定义一个侧输出流标签
.sideOutputLateData(lateTag)

//开窗聚合的打印
windowAggStream.print("Agg");
//侧输出流的打印
 windowAggStream.getSideOutput(lateTag).print("late");
--增量函数AggregateFunction<in,聚合,out>

--全窗口函数WindowFunction<in,out,k,w>
获得windowEnd:timeWindow.getEnd()
迭代器拿出从增量函数处理的值:iterable.iterator().next()
输出:collector.collect()

--KeyedProcessFunction<key,in,out>
收集同一窗口count数据,等待排序输出  
迟到数据出现,会存在刷榜单的情况 因此用map
定义MapState
open
processElement
onTimer

3.1.1 测试

注
//.keyBy("url")   //url是Tuple类型
.keyBy(ApacheLogEvent::getUrl) //另一种写法  这样key就是String类型 关系到自定义全窗口函数的参数

NetworkFlowTest.csv测试文件

乱序数据中,迟到数据出现,会存在刷榜单的情况,如果去除重复部分?

不使用list,使用map,以key-value形式

14 节- 17:16 后 自行分析数据具体延迟、侧输出流,watermark时间

image-20220221143851887

3.2 网站总浏览量PV统计 PageView

pv,访问一次网站,count+1

不需要分组

解决数据倾斜问题

  • Module - NetworkFlowAnalysis
  • resources - UserBehavior.csv :埋点日志,存有pv数
  • PageView分析类: String -> UserBehavior -> PageViewCount
注:在 4分组开窗中 
前面几个分析类是针对商品或页面,需要对itemid、url分组
现在网站总浏览量PV统计不需要分组
但是开窗步骤前 需要分组,怎么解决?
一个简单思路:
使用.map()方法,将数据转化为二元组,key-value形式(pv-1),然后在.keyBy()对0位置进行分组(按照输入的索引0),其实也就是把它们全部分到一个窗口
.map(new MapFunction<UserBehavior, Tuple2<String, Long>>() {
    @Override
    public Tuple2<String, Long> map(UserBehavior userBehavior) throws Exception {
        return new Tuple2<>("pv",1L);
    }
})
.keyBy(0)

3.2.1 测试

.map()  把数据转化为二元组,key:pv  value   的形式输出
.sum(1)   给二元组的位置1做求和  sum方法输入输出类型不能改变

输出每个窗口统计出来的pv个数

image-20220221154944588

改进:

1、给输出加上窗口信息:用aggregate

2、数据倾斜问题:且调大并行度没有用:因为keyBy根据hashcode重分区,现在所有元素都.map成了一个key,所以分组都分到了同一个分区,无法并行。解决:要重新换一个key,能不能以itemid/userid作为key?

—不能:没有必要,且万一同一个itemid的数据量特别大,会对分区压力太大,没有及时处理完,而其他分区早已处理完。这也是大数据处理的数据倾斜问题。所以要考虑换一个key且使其平均分布各分区。

3.2.2 解决数据倾斜

当不需要特定的key时,可能会出现数据倾斜问题

解决数据倾斜:

随机生成1-10数字作为key,进行处理求和,最后再把各个key进行汇总,解决数据倾斜问题

.map(new MapFunction<UserBehavior, Tuple2<Integer, Long>>() {   //以Integer整数作为key
    @Override
    public Tuple2<Integer, Long> map(UserBehavior userBehavior) throws Exception {
        Random random = new Random();
        return new Tuple2<>(random.nextInt(10), 1L);   //生成1-10随机数作为key
    }
.keyBy(data -> data.f0)   //f0:二元组位置0

给输出加上窗口信息:用aggregate两个new,聚合为PageViewCount类

image-20220221172005482

改进:

最后:同一个窗口会重复输出,现在需要只输出一次:.process()

3.2.3 解决同一窗口重复输出

最后使用.process(),定义ValueState保存count,并设置定时器延迟触发输出

---函数方法  extends KeyedProcessFunction<key I o>
ValueState :定义一个状态 保存当前的总count值 这里就没必要拿一个list、map把数全存下来,只要叠加count,最后输出就行
open
processElement
onTimer

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2Vc9Izl-1653008673145)(flink笔记.assets/image-20220301093700597.png)]

每个窗口统一输出一次结果

image-20220221173950681

3.2.1 自动推断

image-20220221154519855

3.3 网站独立访客数UV统计 UniqueVisitor

​ 在前面,我们统计的是所有用户对页面的所有浏览行为,也就是说,同一用户的浏览行为会被重复统计。而在实际应用中,我们往往还会关注,在一段时间内到底有多少不同的用户访问了网站

​ 网站的独立访客数(Unique Visitor,UV)UV 指的是一段时间(比如一小时)内访问网站的总人数,1 天内同一访客的多次访问只记录为一个访客。通过 IP 和 cookie 一般是判断 UV 值的两种方式。当客户端第一次访问某个网站服务器的时候,网站服务器会给这个客户端的电脑发出一个 Cookie,通常放在这个客户端电脑的 C 盘当中。在这个 Cookie 中会分配一个独一无二的编号,这其中会记录一些访问服务器的信息,如访问时间,访问了哪些页面等等。当你下次再访问这个服务器的时候,服务器就可以直接从你的电脑中找到上一次放进去的Cookie 文件,并且对其进行一些更新,但那个独一无二的编号是不会变的。

​ 当然,对于 UserBehavior 数据源来说,我们直接可以根据 userId 来区分不同的用户

思路:简单的使用全窗口函数,收集齐后给出结果

//一般不建议用全窗口 但并行度为1 可以用


---函数方法  implements AllWindowFunction
前面用的.timeWindowAll,所以这里继承的是AllWindowFunction<IN, OUT, W>
//定义一个set结构,保存窗口中的所有userid,自动去重
HashSet<Long> uidSet = new HashSet<>();
  • UniqueVisitor分析类: String -> UserBehavior -> PageViewCount

3.3.1 使用布隆过滤器存储userid UvWithBloomFilter ???

前面是吧数据放在内存中,可能存在内存不够的情况

所以利用布隆过滤器,把数据放在redis

来一个数,在redis判断一下是否存在,在的话不操作;不在的话count+1,且布隆过滤器对应位置1,表示存在

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.1</version>
</dependency>

运行redis

redis-server.exe redis.windows.conf
image-20220227163542615

4 市场营销商业指标统计分析

新建一个 maven module 作为子项目,命名为MarketAnalysis。

这个模块中我们没有现成的数据,所以会用自定义的测试源implements SourceFunction<要求的数据类型>, 来产生测试数据流,或者直接用生成测试数据文件。

4.1 APP 市场推广统计-分渠道-AppMarketingByChannel

统计:某时间段内,通过某渠道channel,用户进行某行为的数量统计count

  • MarketingUserBehavior实体类POJO:(用户行为)从哪个渠道进行了什么行为
  • ChannelPromotionCount实体类:类似于ItemViewCount
  • AppMarketingByChannel分析类:用到函数ProcessWindowFunction<in,out,k,w>,它可以拿到上下文,因此做示例
自定义数据源
implements SourceFunction<MarketingUserBehavior>
--第二个new使用:函数ProcessWindowFunction<in,out,k,w>:
可以获得上下文context,其context可以拿到数据比全窗口函数的window更加丰富
如何获得windowEnd?
String windowEnd = new Timestamp(context.window().getEnd()).toString();  
//new Timestamp()转为时间戳  再toString

得到从哪个渠道,进行何行为,窗口结束时间,数量

image-20220228151253981

4.2 APP 市场推广统计-不分渠道-总量-AppMarketingStatistics

业务中,在统计完各渠道、行为后,还需要一个总量count(不需要渠道和行为)

统计:推广总量

  • AppMarketingStatistics分析类
开窗统计中,用map方法转出二元组
.map(new MapFunction<MarketingUserBehavior, Tuple2<String, Long>>()

相当于每来一个数据 做一次统计

输出:随着 时间推移,窗口内来一个数据加count一个,没有渠道和行为,只有总量count

image-20220228155414947

4.3 页面广告分析

电商网站的市场营销商业指标中,除了自身的 APP 推广,还会考虑到页面上的广告投放(包括自己经营的产品和其它网站的广告)。

4.3.1 页面广告点击量统计

统计页面广告的点击量count

根据用户的地理位置进行划分,从而总结出不同省份用户对不同广告的偏好

  • AdStatisticsByProvince分析类
  • resources-AdClickLog.csv:数据源 => 用户id、广告id、省、市、时间戳
  • AdClickEvent实体类:对应数据源的数据类型
  • AdCountViewByProvince实体类:
image-20220228193651631

4.3.2 改进-黑名单过滤⭐

如果用户在一段时间非常频繁地点击广告,这显然不是一个正常行为,有刷点击量的嫌疑。所以我们可以对一段时间内(比如一天内)的用户点击行为进行约束,如果对同一个广告点击超过一定限额(比如 100 次),应该把该用户加入黑名单并报警,此后其点击行为不应该再统计。

主流进行数据处理,测流输出报警信息

  • BlackListUserWarning实体类 侧输出流输出的报警信息实体类

---KeyedProcessFunction<key,in,out>⭐
   对同一个用户点击同一个广告的行为进行检测报警
   定义ValueState   保存当前用户对某广告的点击次数  只有一个值 用ValueState
   定义ValueState   定义标志状态 保存当前用户是否已经被发送到了黑名单 |  如果没有次标志 当达到上限时,每点击一次就会报警一次                    需求只要报警一次然后不输出到后续数据处理
   open            
   				   //open生命周期中 具体状态
   processElement
   				   // 1 判断是否是第一个数据 如果是的话 注册一个第二天0点的定时器 第二天0点对状态清零
   				   // 2 判断是否达到上限
   onTimer
   				   // 清空所有状态

5 恶意登录监控

对于网站而言,用户登录并不是频繁的业务操作。如果一个用户短时间内频繁登录失败,就有可能是出现了程序的恶意攻击,比如密码暴力破解。因此我们考虑,应该对用户的登录失败动作进行统计,具体来说,如果同一用户(可以是不同 IP)在 2 秒之内连续两次登录失败,就认为存在恶意登录的风险,输出相关的信息进行报警提示。这是电商网站、也是几乎所有网站风控的基本一环。

  • modul–LoginFailDetect
  • LoginEvent实体类
  • LoginFailWarning实体类
  • LoginFail分析

5.1 状态编程

---extends KeyedProcessFunction
	注册一个2秒的定时器,两秒钟内超过2次失败登录,会触发定时器报警
	定义状态ListState 保存2s内失败事件
	定义状态timerTsState 保存注册的定时器时间戳
	processElement:
				判断当前登录事件类型 
				失败:把失败事件添加到状态  并检测它是否有定时器 没有的话注册一个
				成功:之前或许失败过一次而注册了定时器,删除定时器,清空状态,重新开始
	onTimer:
				定时器触发 2s中没有登录成功  判断状态loginEventListState中失败次数
				if 失败次数大于上限 报警
				无论是否if进去,2s时间已过,清空状态  重新开始

5.1.1 报错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9W6QVr5m-1653008673145)(flink笔记.assets/image-20220303210549648.png)]

添加依赖
import org.apache.flink.shaded.guava18.com.google.common.collect.Lists;

5.1.2 改进-把定时器改用事件时间

直接把每次登录失败的数据存起来、设置定时器一段时间后再读取,这种做法尽管简单,但和我们开始的需求还是略有差异的。

比如:设置2s的定时器,但是如果在黑客在1s中内连续进行100次恶意登录,2s后才能报警,甚至1s内的100次恶意登录如果成功一次,则不会报警。这明显存在漏洞。

改进:不用定时器,使用事件时间,来一个数据检测和上一条数据的事件时间间隔,超出上限则报警

新写的LoginFailDetectWarning方法

**缺点:**这个方法目前仅仅只能实现的是检测“连续 2 次登录失败”,如果3次4次代码逻辑要改的更多,因此此时传的参数2没有意义。

并且无法处理乱序数据

5.2 CEP 编程-复杂事件处理

上一节我们通过对状态编程的改进,去掉了定时器,在 process function 中做了更多的逻辑处理,实现了最初的需求。不过这种方法里有很多的条件判断,而我们目前仅仅实现的是检测“连续 2 次登录失败”,这是最简单的情形。如果需要检测更多次,内部逻辑显然会变得非常复杂。那有什么方式可以方便地实现呢?

很幸运,flink 为我们提供了 CEP(Complex Event Processing,复杂事件处理)库,用于在流中筛选符合某种复杂模式的事件。接下来我们就基于 CEP 来完成这个模块的实现。

---- CEP(Complex Event Processing,复杂事件处理)库    引入依赖
    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>
  • LoginFailWithCep

6 Flink CEP

得到从哪个渠道,进行何行为,窗口结束时间,数量

image-20220228151253981

4.2 APP 市场推广统计-不分渠道-总量-AppMarketingStatistics

业务中,在统计完各渠道、行为后,还需要一个总量count(不需要渠道和行为)

统计:推广总量

  • AppMarketingStatistics分析类
开窗统计中,用map方法转出二元组
.map(new MapFunction<MarketingUserBehavior, Tuple2<String, Long>>()

相当于每来一个数据 做一次统计

输出:随着 时间推移,窗口内来一个数据加count一个,没有渠道和行为,只有总量count

image-20220228155414947

4.3 页面广告分析

电商网站的市场营销商业指标中,除了自身的 APP 推广,还会考虑到页面上的广告投放(包括自己经营的产品和其它网站的广告)。

4.3.1 页面广告点击量统计

统计页面广告的点击量count

根据用户的地理位置进行划分,从而总结出不同省份用户对不同广告的偏好

  • AdStatisticsByProvince分析类
  • resources-AdClickLog.csv:数据源 => 用户id、广告id、省、市、时间戳
  • AdClickEvent实体类:对应数据源的数据类型
  • AdCountViewByProvince实体类:
image-20220228193651631

4.3.2 改进-黑名单过滤⭐

如果用户在一段时间非常频繁地点击广告,这显然不是一个正常行为,有刷点击量的嫌疑。所以我们可以对一段时间内(比如一天内)的用户点击行为进行约束,如果对同一个广告点击超过一定限额(比如 100 次),应该把该用户加入黑名单并报警,此后其点击行为不应该再统计。

主流进行数据处理,测流输出报警信息

  • BlackListUserWarning实体类 侧输出流输出的报警信息实体类

---KeyedProcessFunction<key,in,out>⭐
   对同一个用户点击同一个广告的行为进行检测报警
   定义ValueState   保存当前用户对某广告的点击次数  只有一个值 用ValueState
   定义ValueState   定义标志状态 保存当前用户是否已经被发送到了黑名单 |  如果没有次标志 当达到上限时,每点击一次就会报警一次                    需求只要报警一次然后不输出到后续数据处理
   open            
   				   //open生命周期中 具体状态
   processElement
   				   // 1 判断是否是第一个数据 如果是的话 注册一个第二天0点的定时器 第二天0点对状态清零
   				   // 2 判断是否达到上限
   onTimer
   				   // 清空所有状态

5 恶意登录监控

对于网站而言,用户登录并不是频繁的业务操作。如果一个用户短时间内频繁登录失败,就有可能是出现了程序的恶意攻击,比如密码暴力破解。因此我们考虑,应该对用户的登录失败动作进行统计,具体来说,如果同一用户(可以是不同 IP)在 2 秒之内连续两次登录失败,就认为存在恶意登录的风险,输出相关的信息进行报警提示。这是电商网站、也是几乎所有网站风控的基本一环。

  • modul–LoginFailDetect
  • LoginEvent实体类
  • LoginFailWarning实体类
  • LoginFail分析

5.1 状态编程

---extends KeyedProcessFunction
	注册一个2秒的定时器,两秒钟内超过2次失败登录,会触发定时器报警
	定义状态ListState 保存2s内失败事件
	定义状态timerTsState 保存注册的定时器时间戳
	processElement:
				判断当前登录事件类型 
				失败:把失败事件添加到状态  并检测它是否有定时器 没有的话注册一个
				成功:之前或许失败过一次而注册了定时器,删除定时器,清空状态,重新开始
	onTimer:
				定时器触发 2s中没有登录成功  判断状态loginEventListState中失败次数
				if 失败次数大于上限 报警
				无论是否if进去,2s时间已过,清空状态  重新开始

5.1.1 报错

[外链图片转存中…(img-9W6QVr5m-1653008673145)]

添加依赖
import org.apache.flink.shaded.guava18.com.google.common.collect.Lists;

5.1.2 改进-把定时器改用事件时间

直接把每次登录失败的数据存起来、设置定时器一段时间后再读取,这种做法尽管简单,但和我们开始的需求还是略有差异的。

比如:设置2s的定时器,但是如果在黑客在1s中内连续进行100次恶意登录,2s后才能报警,甚至1s内的100次恶意登录如果成功一次,则不会报警。这明显存在漏洞。

改进:不用定时器,使用事件时间,来一个数据检测和上一条数据的事件时间间隔,超出上限则报警

新写的LoginFailDetectWarning方法

**缺点:**这个方法目前仅仅只能实现的是检测“连续 2 次登录失败”,如果3次4次代码逻辑要改的更多,因此此时传的参数2没有意义。

并且无法处理乱序数据

5.2 CEP 编程-复杂事件处理

上一节我们通过对状态编程的改进,去掉了定时器,在 process function 中做了更多的逻辑处理,实现了最初的需求。不过这种方法里有很多的条件判断,而我们目前仅仅实现的是检测“连续 2 次登录失败”,这是最简单的情形。如果需要检测更多次,内部逻辑显然会变得非常复杂。那有什么方式可以方便地实现呢?

很幸运,flink 为我们提供了 CEP(Complex Event Processing,复杂事件处理)库,用于在流中筛选符合某种复杂模式的事件。接下来我们就基于 CEP 来完成这个模块的实现。

---- CEP(Complex Event Processing,复杂事件处理)库    引入依赖
    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>
  • LoginFailWithCep

6 Flink CEP

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值