最全Flink1,2024年最新大数据开发基础开发入门

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

public class TransFilter {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<WaterSensor> source = env.fromElements(
                new WaterSensor("sensor_1", 1L, 1),
                new WaterSensor("sensor_1", 2L, 2),
                new WaterSensor("sensor_2", 2L, 2),
                new WaterSensor("sensor_3", 3L, 3)
        );
//      方式二:传入FilterFunction实现类
        source.filter(new UserFilter()).print();
        env.execute();

    }
    private static class UserFilter implements FilterFunction<WaterSensor> {
        @Override
        public boolean filter(WaterSensor value) throws Exception {
            return value.id.equals("sensor_1");
        }
    }
}
2.富函数类

“富函数类”也是DataStream API提供的一个函数类的接口,所有的Flink函数类都有其Rich版本。富函数类一般是以抽象类的形式出现的。例如:RichMapFunction、RichFilterFunction、RichReduceFunction等。

与常规函数类的不同主要在于,富函数类可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

Rich Function有生命周期的概念。典型的生命周期方法有:

open()方法,是Rich Function的初始化方法,也就是会开启一个算子的生命周期。当一个算子的实际工作方法例如map()或者filter()方法被调用之前,open()会首先被调用。

close()方法,是生命周期中的最后一个调用的方法,类似于结束方法。一般用来做一些清理工作。

需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;而对应的,实际工作方法,例如RichMapFunction中的map(),在每条数据到来后都会触发一次调用。

public class RichFunctionExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);
        DataStreamSource<Integer> source = env.fromElements(1, 2, 3, 4);
        source.map(new RichMapFunction<Integer, Integer>() {
            @Override
            public void open(Configuration parameters) throws Exception {
                super.open(parameters);
                System.out.println("索引是:" + getRuntimeContext().getIndexOfThisSubtask() + " 的任务的生命周期开始");
            }
            @Override
            public Integer map(Integer value) throws Exception {
                return value+1;
            }
            @Override
            public void close() throws Exception {
                super.close();
                System.out.println("索引是:" + getRuntimeContext().getIndexOfThisSubtask() + " 的任务的生命周期结束");
            }
        }).print();
        env.execute();
    }
}
4.物理分区算子
1.随机分区(shuffle)

最简单的重分区方式就是直接“洗牌”。通过调用DataStream的.shuffle()方法,将数据随机地分配到下游算子的并行任务中去。

随机分区服从均匀分布(uniform distribution),所以可以把流中的数据随机打乱,均匀地传递到下游任务分区。因为是完全随机的,所以对于同样的输入数据, 每次执行得到的结果也不会相同。

经过随机分区之后,得到的依然是一个DataStream。

public class ShuffleExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        KeyedStream<String, String> stringStringKeyedStream =
                source.keyBy(value -> value);
        DataStream<String> shuffle = source.shuffle();
        stringStringKeyedStream.print("key");
        shuffle.print("shuffle");
        env.execute();
    }
}
2.轮询分区(Round-Robin)

轮询,简单来说就是“发牌”,按照先后顺序将数据做依次分发。通过调用DataStream的.rebalance()方法,就可以实现轮询重分区。rebalance使用的是Round-Robin负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去。

stream.rebalance()
3.重缩放分区(rescale)

重缩放分区和轮询分区非常相似。当调用rescale()方法时,其实底层也是使用Round-Robin算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中。rescale的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌。

stream.rescale()
4.广播(broadcast)

这种方式其实不应该叫做“重分区”,因为经过广播之后,数据会在不同的分区都保留一份,可能进行重复处理。可以通过调用DataStream的broadcast()方法,将输入数据复制并发送到下游算子的所有并行任务中去。

stream.broadcast()
5.全局分区(global)

全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用.global()方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。

stream.global()
6.自定义分区(Custom)

当Flink提供的所有分区策略都不能满足用户的需求时,我们可以通过使用partitionCustom()方法来自定义分区策略。

public class PartitionCustomDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        source.partitionCustom(new Partitioner<String>() {
            @Override
            public int partition(String key, int numPartitions) {
                return Integer.parseInt(key)%numPartitions;
            }
        }, new KeySelector<String, String>() {
            @Override
            public String getKey(String value) throws Exception {
                return value;
            }
        }).print();
        env.execute();
    }
}
5.分流
1.简单实现

所谓“分流”,就是将一条数据流拆分成完全独立的两条、甚至多条流。也就是基于一个DataStream,定义一些筛选条件,将符合条件的数据拣选出来放到对应的流里。

只要针对同一条流多次独立调用.filter()方法进行筛选,就可以得到拆分之后的流了

案例:读取一个整数数字流,将数据流划分为奇数流和偶数流。

//      读取一个整数数字流,将数据流划分为奇数流和偶数流。
public class SplitStreamByFilter {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> source = env.fromSource(KafkaSource.<String>builder()
                .setBootstrapServers("hadoop102:9092")
                .setGroupId("jajdk")
                .setTopics("topic_sink")
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .setStartingOffsets(OffsetsInitializer.latest())
                .build(),WatermarkStrategy.noWatermarks(), "kafkasource");
        source.filter(new FilterFunction<String>() {
            @Override
            public boolean filter(String value) throws Exception {
                return Integer.parseInt(value) % 2 == 0;
            }
        }).print("偶数");

        source.filter(new FilterFunction<String>() {
            @Override
            public boolean filter(String value) throws Exception {
                return Integer.parseInt(value) % 2 ==1;
            }
        }).print("基数");
        env.execute();
    }
}

缺点:是将原始数据流stream复制三份,然后对每一份分别做筛选;这明显是不够高效的。

2.使用侧输出流

需要调用上下文ctx的.output()方法,就可以输出任意类型的数据了。而侧输出流的标记和提取,都离不开一个“输出标签”(OutputTag),指定了侧输出流的id和类型。

案例:将id等于s1和s2的单独输出

//侧输出流
public class SplitStreamByOutputTag {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> source = env.fromSource(KafkaSource.<String>builder()
                .setBootstrapServers("hadoop102:9092,hadoop103:9092")
                .setTopics("topic_sink")
                .setGroupId("yjh")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .build(), WatermarkStrategy.noWatermarks(), "kafasource");
        SingleOutputStreamOperator<WaterSensor> map = source.map(new MapFunction<String, WaterSensor>() {
            @Override
            public WaterSensor map(String value) throws Exception {
                String[] split = value.split(",");
                return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            }
        });
        OutputTag<WaterSensor> s1 = new OutputTag<>("s1", Types.POJO(WaterSensor.class));
        OutputTag<WaterSensor> s2 = new OutputTag<>("s2", Types.POJO(WaterSensor.class));
        SingleOutputStreamOperator<WaterSensor> process = map.process(new ProcessFunction<WaterSensor, WaterSensor>() {
            @Override
            public void processElement(WaterSensor value, ProcessFunction<WaterSensor, WaterSensor>.Context ctx, Collector<WaterSensor> out) throws Exception {
                if ("s1".equals(value.id)) {
                    ctx.output(s1, value);
                } else if ("s2".equals(value.id)) {
                    ctx.output(s2, value);
                } else {
                    out.collect(value);
                }
            }
        });
        SideOutputDataStream<WaterSensor> s1Output = process.getSideOutput(s1);
        SideOutputDataStream<WaterSensor> s2Output = process.getSideOutput(s2);
        s1Output.print("s1");
        s2Output.print("s2");
        process.print();
        env.execute();
    }
}
6.合流
1.联合(Union)

最简单的合流操作,就是直接将多条流合在一起,叫作流的“联合”(union)。联合操作要求必须流中的数据类型必须相同,合并之后的新流会包括所有流中的元素,数据类型不变。

public class UnionExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Integer> ds1 = env.fromElements(1, 2, 3);
        DataStreamSource<Integer> ds2 = env.fromElements(10, 20, 30);
        DataStreamSource<String> ds3 = env.fromElements("4", "5", "8");
        DataStream<Integer> union = ds1.union(ds2, ds3.map(Integer::valueOf));
        union.print();
        env.execute();
    }
}
2.连接(Connect)

流的联合虽然简单,不过受限于数据类型不能改变,灵活性大打折扣,所以实际应用较少出现。除了联合(union),Flink还提供了另外一种方便的合流操作——连接(connect)。

public class ConnectDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> ds1 = env.fromSource(KafkaSource.<String>builder()
                .setBootstrapServers("hadoop102:9092,hadoop103:9092")
                .setGroupId("yjh")
                .setTopics("topic_sink")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .build(), WatermarkStrategy.noWatermarks(), "kafkasource");
//        ds1.print("ds1");
        DataStreamSource<Integer> ds2 = env.fromElements(1, 2, 3, 5);
        ConnectedStreams<String, Integer> connect = ds1.connect(ds2);
        connect.map(new CoMapFunction<String, Integer, String>() {
            @Override
            public String map1(String value) throws Exception {
                return value;
            }
            @Override
            public String map2(Integer value) throws Exception {
                return value.toString();
            }
        }).print();
        env.execute();
    }
}

案例:连接两条流,输出能根据id匹配上的数据(类似inner join效果)

public class ConnectKeybyDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Tuple2<Integer, String>> source1 = env.fromElements(
                Tuple2.of(1, "a1"),
                Tuple2.of(1, "a2"),
                Tuple2.of(2, "b"),
                Tuple2.of(3, "c")
        );
        DataStreamSource<Tuple3<Integer, String, Integer>> source2 = env.fromElements(
                Tuple3.of(1, "aa1", 1),
                Tuple3.of(1, "aa2", 2),
                Tuple3.of(2, "bb", 1),
                Tuple3.of(3, "cc", 1)
        );
        ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>> connect = source1.connect(source2);

        ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>> tuple2Tuple3ConnectedStreams = connect.keyBy(new KeySelector<Tuple2<Integer, String>, Integer>() {
            @Override
            public Integer getKey(Tuple2<Integer, String> value) throws Exception {
                return value.f0;
            }
        }, new KeySelector<Tuple3<Integer, String, Integer>, Integer>() {
            @Override
            public Integer getKey(Tuple3<Integer, String, Integer> value) throws Exception {
                return value.f0;
            }
        });
        tuple2Tuple3ConnectedStreams.process(new CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>() {

           Map<Integer, List<Tuple2<Integer,String>>> cache1  = new HashMap<>();
           Map<Integer,List<Tuple3<Integer,String,Integer>>> cache2  = new HashMap<>();
            @Override
            public void processElement1(Tuple2<Integer, String> value, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
                Integer f0 = value.f0;
                if(!cache1.containsKey(f0)){
                    List<Tuple2<Integer, String>> tuple1s = new ArrayList<>();
                    tuple1s.add(value);
                    cache1.put(f0,tuple1s);
                }else {
                    cache1.get(f0).add(value);
                }
                if(cache2.containsKey(f0)){
                    for (Tuple3<Integer, String, Integer> integerStringIntegerTuple3 : cache2.get(f0)) {
                        out.collect("s1:" + value + "<--------->s2:" + cache2);
                    }
                }
            }

            @Override
            public void processElement2(Tuple3<Integer, String, Integer> value, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, String>.Context ctx, Collector<String> out) throws Exception {

                Integer f0 = value.f0;
                if(!cache2.containsKey(f0)){
                    ArrayList<Tuple3<Integer, String, Integer>> tuple3s = new ArrayList<>();
                    tuple3s.add(value);
                    cache2.put(f0,tuple3s);
                }else {
                    cache2.get(f0).add(value);
                }
                if(cache1.containsKey(f0)){
                    for (Tuple2<Integer, String> integerStringTuple2 : cache1.get(f0)) {
                        out.collect("s2:" + value + "<--------->s1:" + cache1);
                    }
                }
            }
        }).print();
        env.execute();
    }
}
4.输出算子(Sink)

行编码: FileSink.forRowFormat(basePath,rowEncoder)。(行式去写)

批量编码: FileSink.forBulkFormat(basePath,bulkWriterFactory)。(列式去写)

1.输出到文件
public class SinkFile {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
//        数据生成器读取数据
        DataStreamSource<String> source = env.fromSource(new DataGeneratorSource<String>(new GeneratorFunction<Long, String>() {
            @Override
            public String map(Long value) throws Exception {
                return null;
            }
        }, Long.MAX_VALUE, RateLimiterStrategy.perSecond(1000), Types.STRING), WatermarkStrategy.noWatermarks(), "data-gensjkljads");

        FileSink<String> ouput = FileSink.forRowFormat(new Path("ouput/fink.txt"), new SimpleStringEncoder<String>("UTF-8"))
                .withOutputFileConfig(OutputFileConfig.builder()
                        .withPartPrefix("atguigu-")
                        .withPartPrefix(".log")
                        .build())
                .withBucketAssigner(new DateTimeBucketAssigner<String>("yyyy-MM-dd HH", ZoneId.systemDefault()))
                .withRollingPolicy(DefaultRollingPolicy.builder().withRolloverInterval(Duration.ofMinutes(1))
                        .withMaxPartSize(new MemorySize(1024 * 1024)).build())
                .build();
        source.sinkTo(ouput);
        env.execute();
    }
}
2.输出到kafka中
public class SinkKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        KafkaSink<String> topic_sink = KafkaSink.<String>builder()
//                设置kafka链接地址
                .setBootstrapServers("hadoop102:9092,hadoop103:9092")
//                反序列化
                .setRecordSerializer(KafkaRecordSerializationSchema.<String>builder()
                        .setTopic("topic_sink")   //topic名称
                        .setValueSerializationSchema(new SimpleStringSchema())  //反序列化方式(string)
                        .build())
//                写到kafka的一致性级别: 精准一次、至少一次
                .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
//                如果是精准一次,必须设置 事务的前缀
                .setTransactionalIdPrefix("yjh-")
//                检查点链接
                .setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10 * 60 * 1000 + "")
                .build();
        source.sinkTo(topic_sink);

        env.execute();
    }
}

自定义序列化器,实现带key的record:

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        env.setRestartStrategy(RestartStrategies.noRestart());

        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);

        source.sinkTo(KafkaSink.<String>builder()
//                链接kafka
                        .setBootstrapServers("hadoop102:9092,hadoop103:9092")
//                事务前缀
                        .setTransactionalIdPrefix("yjh-")
//                精准消费
                        .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
//                错误最大时长
                        .setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG,10 * 60 * 1000 + "")
//                自定义序列化
                        .setRecordSerializer(new KafkaRecordSerializationSchema<String>() {
                            @Nullable
                            @Override
                            public ProducerRecord<byte[], byte[]> serialize(String element, KafkaSinkContext context, Long timestamp) {
                                String[] split = element.split(",");
                                byte[] key = split[0].getBytes(StandardCharsets.UTF_8);
                                byte[] value = element.getBytes(StandardCharsets.UTF_8);
                                return new ProducerRecord<byte[], byte[]>("topic_sink",key,value);
                            }
                        })
                .build());
        env.execute();
    }
}
3.输出到mysql
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WaterSensor {
    public String id;
    public Long ts;
    public Integer vc;
}
public class SinkMySQL {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(new MapFunction<String, WaterSensor>() {
            @Override
            public WaterSensor map(String value) throws Exception {
                String[] split = value.split(",");
                return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            }
        });

        SinkFunction<WaterSensor> sink = JdbcSink.sink("insert into ws(id,ts,vc) values(?,?,?)", new JdbcStatementBuilder<WaterSensor>() {
            @Override
            public void accept(PreparedStatement preparedStatement, WaterSensor t) throws SQLException {
                preparedStatement.setString(1, t.getId());
                preparedStatement.setLong(2, t.getTs());
                preparedStatement.setInt(3, t.getVc());
            }
        }, JdbcExecutionOptions.builder()
                .withMaxRetries(3)
                .withBatchSize(100)
                .withBatchIntervalMs(3000)
                .build(), new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                .withUrl("jdbc:mysql://1.94.41.70:3306/flink_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8")
                .withUsername("root")
                .withPassword("000000")
                .withConnectionCheckTimeoutSeconds(60)
                .build());

        map.addSink(sink);

        env.execute();
    }
}
4.自定义Sink输出
stream.addSink(new MySinkFunction<String>());

5.Flink中的时间和窗口

1.窗口
1.窗口的概念

Flink中窗口并不是静态准备好的,而是动态创建——当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。

2.窗口的分类
1.按照驱动类型分类
1.时间窗口
2.计数窗口
2.按照窗口分配数据的规则分类
1.滚动窗口

滚动窗口有固定的大小,是一种对数据进行“均匀切片”的划分方式。窗口之间没有重叠,也不会有间隔,是“首尾相接”的状态。这是最简单的窗口形式,每个数据都会被分配到一个窗口,而且只会属于一个窗口。

使用场景:滚动窗口应用非常广泛,它可以对每个时间段做聚合统计,很多BI分析指标都可以用它来实现。

2.滑动窗口

滑动窗口的大小也是固定的。但是窗口之间并不是首尾相接的,而是可以“错开”一定的位置。 定义滑动窗口的参数有两个:除去窗口大小 (window size)之外,还有一个“滑动步长(window slide)它其实就代表了窗口计算的频率。窗口在结束时间触发计算输出结果,那么滑动步长就代表了计算频率,当滑动步长小于窗口大小时,滑动窗口就会出现重叠这时数据也可能会被同时分配到多个窗口中。而具体的个数,就由窗口大小和滑动步长的比值(size/slide) 来决定 使用场景:滚动窗口也可以看作是一种特殊的滑动窗口一一窗口大小等于滑动步长(size = slide)滑动窗口适合计算结果更新频率非常高的场景

3.会话窗口

会话窗口,是基于“会话”(session)来来对数据进行分组的。会话窗口只能基于时间来定义会话窗口中,最重要的参数就是会话的超时时间,也就是两个会话窗口之间的最小距离。如果相邻两个数据到来的时间间隔(Gap)小于指定的大小 (size),那说明还在保持会话,它们就属于同一个窗口;如果ap大于size,那么新来的数据就应该属于新的会话窗口,而前一个窗口就应该关闭了。

会话窗口的长度不固定起始和结束时间也是不确定的,各个分区之间窗口没有任何关联。会话窗口之间一定是不会重叠的,而且会留有至少为size的间隔(sessiongap 在一些类似保持会话的场景下,可以使用会话窗口来进行数据的处理统计。

4.全局窗口

“全局窗口”这种窗口全局有效,会把相同key的所有数据都分配到同一个窗口中。这种窗口没有结束的时候默认是不会做触发计算的。如果希望它能对数据进行计算处理,还需要自定义“触发器(Trigger )

3.窗口API概览
1)按键分区(Keyed)和非按键分区(Non-Keyed)

1.按键分区

stream.keyBy(...)
       .window(...)

2.非按键分区

stream.windowAll(...)
2)代码中窗口API的调用

窗口操作主要有两个部分:窗口分配器(Window Assigners)和窗口函数(Window Functions)。

stream.keyBy(<key selector>)
	.window(<window assigner>)
	.aggregate(<window function>)
4.窗口分配器
1.时间窗口

1.滚动处理时间窗口

窗口分配器由类TumblingProcessingTimeWindows提供,需要调用它的静态方法.of()。

stream.keyBy(...)
       .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
       .aggregate(...)

这里.of()方法需要传入一个Time类型的参数size,表示滚动窗口的大小,我们这里创建了一个长度为5秒的滚动窗口。

另外,.of()还有一个重载方法,可以传入两个Time类型的参数:size和offset。第一个参数当然还是窗口大小,第二个参数则表示窗口起始点的偏移量。

2.滚动处理时间窗口

窗口分配器由类SlidingProcessingTimeWindows提供,同样需要调用它的静态方法.of()。

stream.keyBy(...)
   .window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(5)))
   .aggregate(...)

这里.of()方法需要传入两个Time类型的参数:size和slide,前者表示滑动窗口的大小,后者表示滑动窗口的滑动步长。我们这里创建了一个长度为10秒、滑动步长为5秒的滑动窗口。

滑动窗口同样可以追加第三个参数,用于指定窗口起始点的偏移量,用法与滚动窗口完全一致。

3.处理时间会话窗口

窗口分配器由类ProcessingTimeSessionWindows提供,需要调用它的静态方法.withGap()或者.withDynamicGap()。

stream.keyBy(...)
    .window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
    .aggregate(...)

这里.withGap()方法需要传入一个Time类型的参数size,表示会话的超时时间,也就是最小间隔session gap。我们这里创建了静态会话超时时间为10秒的会话窗口。

另外,还可以调用withDynamicGap()方法定义session gap的动态提取逻辑。

4.滚动事件时间窗口

窗口分配器由类TumblingEventTimeWindows提供,用法与滚动处理事件窗口完全一致。

stream.keyBy(...)
	.window(TumblingEventTimeWindows.of(Time.seconds(5)))
 	.aggregate(...)

5.滑动事件时间窗口

窗口分配器由类SlidingEventTimeWindows提供,用法与滑动处理事件窗口完全一致。

stream.keyBy(...)
	.window(SlidingEventTimeWindows.of(Time.seconds(10),Time.seconds(5)))
	.aggregate(...)

6.事件时间会话窗口

窗口分配器由类EventTimeSessionWindows提供,用法与处理事件会话窗口完全一致。

stream.keyBy(...)
	.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
	.aggregate(...)
2.计数窗口

(1)滚动计数窗口

滚动计数窗口只需要传入一个长整型的参数size,表示窗口的大小。

stream.keyBy(...)
    .countWindow(10)

我们定义了一个长度为10的滚动计数窗口,当窗口中元素数量达到10的时候,就会触发计算执行并关闭窗口。

(2)滑动计数窗口

与滚动计数窗口类似,不过需要在.countWindow()调用时传入两个参数:size和slide,前者表示窗口大小,后者表示滑动步长。

stream.keyBy(...)
    .countWindow(10,3)

我们定义了一个长度为10、滑动步长为3的滑动计数窗口。每个窗口统计10个数据,每隔3个数据就统计输出一次结果。

3)全局窗口

全局窗口是计数窗口的底层实现,一般在需要自定义窗口时使用。它的定义同样是直接调用.window(),分配器由GlobalWindows类提供。

stream.keyBy(...)
    .window(GlobalWindows.create());

需要注意使用全局窗口,必须自行定义触发器才能实现窗口计算,否则起不到任何作用。

5.窗口函数
1.增量聚合函数(ReduceFunction / AggregateFunction)

1.归约函数(ReduceFunction)

public class WindowReduceDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000L, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            WaterSensor waterSensor = new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            return waterSensor;
        });
        KeyedStream<WaterSensor, String> keyedStream = map.keyBy(new KeySelector<WaterSensor, String>() {
            @Override
            public String getKey(WaterSensor value) throws Exception {
                return value.id;
            }
        });
        SingleOutputStreamOperator<WaterSensor> reduce = keyedStream.reduce(new ReduceFunction<WaterSensor>() {
            @Override
            public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {

                return new WaterSensor(value1.id, System.currentTimeMillis(), value1.vc + value2.vc);
            }
        });
        reduce.print();
        env.execute();
    }
}

2.聚合函数(AggregateFunction)

public class WindowAggregateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
        });
        KeyedStream<WaterSensor, String> stream = map.keyBy(new KeySelector<WaterSensor, String>() {
            @Override
            public String getKey(WaterSensor value) throws Exception {
                return value.id;
            }
        });
        WindowedStream<WaterSensor, String, TimeWindow> window = stream.window(TumblingProcessingTimeWindows.of(Time.seconds(2L)));

        SingleOutputStreamOperator<String> aggregate = window.aggregate(new AggregateFunction<WaterSensor, Integer, String>() {
//         创建累加器   初始化累加器
            @Override
            public Integer createAccumulator() {
                System.out.println("创建累加器");
                return 0;
            }
//            计算存储,聚合逻辑
            @Override
            public Integer add(WaterSensor value, Integer accumulator) {
                System.out.println("调用add方法,value=" + value);
                return accumulator + value.getVc();
            }
//            获取最终结果,窗口触发时输出
            @Override
            public String getResult(Integer accumulator) {
                System.out.println("调用getResult方法");
                return accumulator.toString();
            }
//            只有会话窗口才会用到(一般用前三个)
            @Override
            public Integer merge(Integer a, Integer b) {
                System.out.println("调用merge方法 ");
                return null;
            }
        });
        aggregate.print();
        env.execute();
    }
}
2.全窗口函数(full window functions)

sensorWS .process()

public class WindowProcessDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.fromSource(KafkaSource.<String>builder()
                .setBootstrapServers("hadoop102:9092")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setGroupId("yjh")
                .setTopics("topic_sink")

                .setValueOnlyDeserializer(new SimpleStringSchema())

                .build(), WatermarkStrategy.noWatermarks(), "kafasource");

        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
        });
        KeyedStream<WaterSensor, String> waterSensorStringKeyedStream = map.keyBy(WaterSensor::getId);

        WindowedStream<WaterSensor, String, TimeWindow> window = waterSensorStringKeyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));

        SingleOutputStreamOperator<String> process = window.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
            @Override
            public void process(String key, ProcessWindowFunction<WaterSensor, String, String, TimeWindow>.Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                long start = context.window().getStart();
                long end = context.window().getEnd();
                String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd HH:mm:ss.SSS");
                String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd HH:mm:ss.SSS");
                long count = elements.spliterator().estimateSize();
                String data = elements.toString();
                out.collect("窗口开始时间:" + windowStart + ",窗口结束时间:" + windowEnd + ",数据个数:" + count + "数据:" + data);
            }
        });
        process.print();

        env.execute();
    }
}
3.增量聚合和全窗口函数的结合使用
public class WindowAggregateAndProcessDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.fromSource(KafkaSource.<String>builder()
                .setBootstrapServers("hadoop102:9092,hadoop103:9092")
                .setTopics("topic_sink")
                .setGroupId("yjh")
                .setStartingOffsets(OffsetsInitializer.latest())
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .build(), WatermarkStrategy.noWatermarks(), "kafkaSource");
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            WaterSensor waterSensor = new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            return waterSensor;
        });
        KeyedStream<WaterSensor, String> waterSensorStringKeyedStream = map.keyBy(WaterSensor::getId);
        WindowedStream<WaterSensor, String, TimeWindow> window = waterSensorStringKeyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(20)));
        SingleOutputStreamOperator<String> aggregate = window.aggregate(new MyAgg(), new Mypocess());
        aggregate.print();
        env.execute();
    }
    private static class MyAgg implements AggregateFunction<WaterSensor,Integer,String> {
        @Override
        public Integer createAccumulator() {
            return 0;
        }
        @Override
        public Integer add(WaterSensor value, Integer accumulator) {
            return value.getVc() + accumulator;
        }
        @Override
        public String getResult(Integer accumulator) {

            return accumulator.toString();
        }
        @Override
        public Integer merge(Integer a, Integer b) {
            return null;
        }
    }
    private static class Mypocess implements WindowFunction<String,String,String,TimeWindow> {
        @Override
        public void apply(String s, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
            long start = window.getStart();
            long end = window.getEnd();
            long count = input.spliterator().estimateSize();
            String startWindow = DateFormatUtils.format(start, "yyyy-MM-dd HH:mm:ss.SSS");
            String endWindow = DateFormatUtils.format(end, "yyyy-MM-dd HH:mm:ss.SSS");
            out.collect("窗口的大小为["+startWindow+","+endWindow+"),个数为:"+count+"数据为"+input.toString());
        }
    }
}
4.其他API
1.触发器(Triger)

触发器主要是用来控制窗口什么时候触发计算。所谓的“触发计算”,本质上就是执行窗口函数,所以可以认为是计算得到结果并输出的过程。

基于WindowedStream调用.trigger()方法,就可以传入一个自定义的窗口触发器(Trigger)。

stream.keyBy(...)
	.window(...)
	.trigger(new MyTrigger())
2.移除器(Evictor)

移除器主要用来定义移除某些数据的逻辑。基于WindowedStream调用.evictor()方法,就可以传入一个自定义的移除器(Evictor)。Evictor是一个接口,不同的窗口类型都有各自预实现的移除器。

stream.keyBy(...)
	.window(...)
	.evictor(new MyEvictor())
2.时间语义

**事件时间:**一个是数据产生的时间(时间戳Timestamp)

**处理时间:**数据真正被处理的时刻

在实际应用中,事件时间语义会更为常见。一般情况下,业务日志数据中都会记录数据生成的时间戳(timestamp),它就可以作为事件时间的判断基础。

3.水位线(Watermark)

在Flink中,用来衡量事件时间进展的标记,就被称作“水位线”(Watermark)。

1.水位线特性

水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据

水位线主要的内容是一个时间戳,用来表示当前事件时间的进展

水位线是基于数据的时间戳生成的

水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进

水位线可以通过设置延迟,,来保证正确处理乱序数据

一个水位线Watermmark(t),表示在当前流中事件时间已经达到了时间戳t,这代表t之前的所有数据都到齐了,之后流中不会出现时间戳t’<t的数据

2.水位线理解
3.什么是水位线

完美的水位线是“绝对正确”的,也就是一个水位线一旦出现,就表示这个时间之前的数据已经全部到齐、之后再也不会出现了。不过如果要保证绝对正确,就必须等足够长的时间,这会带来更高的延迟。

**如果我们希望处理得更快、实时性更强,那么可以将水位线延迟设得低一些。**这种情况下,可能很多迟到数据会在水位线之后才到达,就会导致窗口遗漏数据,计算结果不准确。当然,如果我们对准确性完全不考虑、一味地追求处理速度,可以直接使用处理时间语义,这在理论上可以得到最低的延迟。

所以Flink中的水位线,其实是流处理中**对低延迟和结果正确性的一个权衡机制,**而且把控制的权力交给了程序员,我们可以在代码中定义水位线的生成策略。

4.Flink内置水位线
1.有序流中内置水位线设置

WatermarkStrategy.forMonotonousTimestamps()

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
        });
        SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = map.assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forMonotonousTimestamps().withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
            @Override
            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                System.out.println(element.toString() + "<----->" + recordTimestamp);
                return element.getTs() * 1000L;
            }
        }));

        KeyedStream<WaterSensor, String> keyBy = waterSensorSingleOutputStreamOperator.keyBy(WaterSensor::getId);
        WindowedStream<WaterSensor, String, TimeWindow> window = keyBy.window(TumblingEventTimeWindows.of(Time.seconds(10)));
        SingleOutputStreamOperator<String> aggregate = window.aggregate(new AggregateFunction<WaterSensor, Integer, String>() {
            @Override
            public Integer createAccumulator() {
                return 0;
            }

            @Override
            public Integer add(WaterSensor value, Integer accumulator) {
                return value.getVc()+accumulator;
            }

            @Override
            public String getResult(Integer accumulator) {
                return accumulator.toString();
            }

            @Override
            public Integer merge(Integer a, Integer b) {
                return null;
            }
        }, new WindowFunction<String, String, String, TimeWindow>() {
            @Override
            public void apply(String s, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
                String start = DateFormatUtils.format(window.getStart(), "yyyy-MM-dd HH:mm:ss.SSS");
                String end = DateFormatUtils.format(window.getEnd(), "yyyy-MM-dd HH:mm:ss.SSS");
                long num = input.spliterator().estimateSize();
                out.collect("窗口:["+start+","+end+")"+",个数:"+num+",数据:"+input.toString());

            }
        });
        aggregate.print();
        env.execute();
    }
}
2.乱序流中内置水位线设置

WatermarkStrategy. forBoundedOutOfOrderness()

public class WatermarkOutOfOrdernessDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            WaterSensor waterSensor = new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            return waterSensor;
        });

        SingleOutputStreamOperator<WaterSensor> watermarks = map
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                System.out.println(element.getTs()*100);
                                return element.getTs()*100;
                            }
                        }));
        
        KeyedStream<WaterSensor, String> waterSensorStringKeyedStream = watermarks.keyBy(WaterSensor::getId);
        WindowedStream<WaterSensor, String, TimeWindow> window = waterSensorStringKeyedStream.window(TumblingEventTimeWindows.of(Time.seconds(1)));

        SingleOutputStreamOperator<String> aggregate = window.aggregate(new AggregateFunction<WaterSensor, Integer, String>() {
            @Override
            public Integer createAccumulator() {
                return 0;
            }
            @Override
            public Integer add(WaterSensor value, Integer accumulator) {
                return value.getVc() + accumulator;
            }
            @Override
            public String getResult(Integer accumulator) {
                return accumulator.toString();
            }
            @Override
            public Integer merge(Integer a, Integer b) {
                return null;
            }
        }, new WindowFunction<String, String, String, TimeWindow>() {
            @Override
            public void apply(String s, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
                String startWindow = DateFormatUtils.format(window.getStart(), "yyyy-MM-dd HH:mm:ss.SSS");
                String envWindow = DateFormatUtils.format(window.getEnd(), "yyyy-MM-dd HH:mm:ss.SSS");
                long num = input.spliterator().estimateSize();
                out.collect("窗口:[" + startWindow + "," + envWindow + ")" + ",个数:" + num + ",数据:" + input.toString());
            }});
        
        aggregate.print();
        env.execute();
        };
    }
3.自定义水位线生成器
public class CustomPeriodicWatermarkExample {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            WaterSensor waterSensor = new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
            return waterSensor;
        });


        SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator
                = map.assignTimestampsAndWatermarks(new CustomWatermarkStrategy());
        waterSensorSingleOutputStreamOperator.print();
        env.execute();
    }

    private static class CustomWatermarkStrategy implements WatermarkStrategy<WaterSensor> {

        @Override
        public WatermarkGenerator<WaterSensor> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new WatermarkGenerator<WaterSensor>() {
                private Long delayTime = 3000L;
                private Long maxTs = -Long.MAX_VALUE+delayTime+1L;


                @Override
                public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
                    System.out.println("eventTimestamp:"+eventTimestamp);
                    long maxTs = Math.max(event.getTs(), eventTimestamp);
                    System.out.println(maxTs);

                }

                @Override
                public void onPeriodicEmit(WatermarkOutput output) {
                    System.out.println("水位线时间:" + (maxTs - delayTime - 1L));
                    output.emitWatermark(new Watermark(maxTs - delayTime - 1L));

                }
            };
        }

        @Override
        public TimestampAssigner<WaterSensor> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<WaterSensor>() {
                @Override
                public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                    return element.getTs();
                }
            };
        }
    }
}
5.水位线的传递
public class WatermarkIdlenessDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        DataStream<String> stringDataStream = source.partitionCustom(new Partitioner<String>() {
            @Override
            public int partition(String key, int numPartitions) {
                return Integer.parseInt(key) % numPartitions;
            }
        }, new KeySelector<String, String>() {
            @Override
            public String getKey(String value) throws Exception {
                System.out.println(value);
                return value;
            }
        });
        stringDataStream.map(Integer::parseInt)
                        .assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)))
                                .keyBy(value -> value % 2)
                                        .window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
                                                .process(new ProcessWindowFunction<Integer, String, Integer, TimeWindow>() {
                                                    @Override
                                                    public void process(Integer integer, ProcessWindowFunction<Integer, String, Integer, TimeWindow>.Context context, Iterable<Integer> elements, Collector<String> out) throws Exception {
                                                        String start = DateFormatUtils.format(context.window().getStart(), "yyyy-MM-dd HH:mm:ss.SSS");
                                                        String end = DateFormatUtils.format(context.window().getEnd(), "yyyy-MM-dd HH:mm:ss.SSS");
                                                        int count = elements.spliterator().characteristics();

                                                        out.collect("key=" + integer + "的窗口[" + start + "," + end + ")包含" + count + "条数据===>" + elements.toString());

                                                    }
                                                })
                                                        .print();

        env.execute();
    }
}
4.迟到数据的处理

1.推迟水位线推进

例:水位线延迟10s推进

WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10));

2.设置窗口延迟关闭

.allowedLateness()

例:窗口延迟3秒关闭

.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))

3.使用测输出流来娄底

.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))
.sideOutputLateData(lateWS)

例:

public class WatermarkLateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
//        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        DataStreamSource<String> source = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<WaterSensor> map = source.map(value -> {
            String[] split = value.split(",");
            return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.parseInt(split[2]));
        });

        SingleOutputStreamOperator<WaterSensor> watermarks = map.assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                    @Override
                    public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                        System.out.println(element.getTs() );
                        return element.getTs() * 1000L ;
                    }
                }));

        KeyedStream<WaterSensor, String> keyedStream = watermarks.keyBy(WaterSensor::getId);

        OutputTag<WaterSensor> waterSensorOutputTag = new OutputTag<>("late-data", Types.POJO(WaterSensor.class));

        WindowedStream<WaterSensor, String, TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .allowedLateness(Time.seconds(3))
                .sideOutputLateData(waterSensorOutputTag);

        SingleOutputStreamOperator<String> process = windowedStream.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
            @Override
            public void process(String s, ProcessWindowFunction<WaterSensor, String, String, TimeWindow>.Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                String start = DateFormatUtils.format(context.window().getStart(), "yyyy-MM-dd HH:mm:ss");
                String end = DateFormatUtils.format(context.window().getEnd(), "yyyy-MM-dd HH:mm:ss");
                System.out.println(start);
                System.out.println(end);
                long count = elements.spliterator().estimateSize();
                out.collect("key=" + s + "的窗口[" + start + "," + end + ")包含" + count + "条数据===>" + elements.toString());
            }
        });

        process.print("正常数据");
        process.getSideOutput(waterSensorOutputTag).printToErr();

        env.execute();
    }
}
5.双流联结(Join)
1.窗口联结(Window Join)

Flink为基于一段时间的双流合并专门提供了一个窗口联结算子,可以定义时间窗口,并将两条流中共享一个公共键(key)的数据放在窗口中进行配对处理。

public class WindowJoinDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        SingleOutputStreamOperator<Tuple2<String, Integer>> stream1 = env.fromElements(
                        Tuple2.of("a", 1),
                        Tuple2.of("a", 2),
                        Tuple2.of("c", 6),
                        Tuple2.of("b", 3),
                        Tuple2.of("d", 2)
                )
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Integer>>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Integer>>() {
                            @Override
                            public long extractTimestamp(Tuple2<String, Integer> element, long recordTimestamp) {
                                return element.f1 * 1000;
                            }
                        }));

        SingleOutputStreamOperator<Tuple3<String, Integer, Integer>> stream2 = env.fromElements(
                Tuple3.of("a", 1, 1),
                Tuple3.of("b", 5, 1),
                Tuple3.of("c", 6, 6),
                Tuple3.of("d", 7, 15),
                Tuple3.of("e", 1, 13),
                Tuple3.of("b", 9, 3),
                Tuple3.of("a", 1, 2)
        ).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String, Integer, Integer>>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String, Integer, Integer>>() {
                    @Override
                    public long extractTimestamp(Tuple3<String, Integer, Integer> element, long recordTimestamp) {
                        return element.f2 * 1000;
                    }
                }));

        DataStream<String> join = stream1.join(stream2)
                .where(value -> value.f0)
                .equalTo(value -> value.f0)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .apply(new JoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>() {
                    @Override
                    public String join(Tuple2<String, Integer> first, Tuple3<String, Integer, Integer> second) throws Exception {
//                        System.out.println(first + "<----->" + second);
                        return first + "<----->" + second;
                    }
                });

        join.print();

        env.execute();
        
    }
}
2.间隔联结(Interval Join)

间隔联结目前只支持事件时间语义

1.正常使用(无乱序)

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

        SingleOutputStreamOperator<Tuple2<String, Integer>> ds1 = env
                .fromElements(
                        Tuple2.of("a", 1),
                        Tuple2.of("a", 2),
                        Tuple2.of("b", 3),
                        Tuple2.of("c", 4)
                )
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy
                                .<Tuple2<String, Integer>>forMonotonousTimestamps()
                                .withTimestampAssigner((value, ts) -> value.f1 * 1000L)
                );

        SingleOutputStreamOperator<Tuple3<String, Integer, Integer>> ds2 = env
                .fromElements(
                        Tuple3.of("a", 1, 1),
                        Tuple3.of("a", 11, 1),
                        Tuple3.of("b", 2, 1),
                        Tuple3.of("b", 12, 1),
                        Tuple3.of("c", 14, 1),
                        Tuple3.of("d", 15, 1)
                )
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy
                                .<Tuple3<String, Integer, Integer>>forMonotonousTimestamps()
                                .withTimestampAssigner((value, ts) -> value.f1 * 1000L)
                );

        // TODO interval join
        //1. 分别做keyby,key其实就是关联条件
        KeyedStream<Tuple2<String, Integer>, String> ks1 = ds1.keyBy(r1 -> r1.f0);
        KeyedStream<Tuple3<String, Integer, Integer>, String> ks2 = ds2.keyBy(r2 -> r2.f0);

        //2. 调用 interval join
        ks1.intervalJoin(ks2)
                .between(Time.seconds(-2), Time.seconds(2))
                .process(
                        new ProcessJoinFunction<Tuple2<String, Integer>, Tuple3<String, Integer, Integer>, String>() {
                            /**
                             * 两条流的数据匹配上,才会调用这个方法
                             * @param left  ks1的数据
                             * @param right ks2的数据
                             * @param ctx   上下文
                             * @param out   采集器
                             * @throws Exception
                             */
                            @Override
                            public void processElement(Tuple2<String, Integer> left, Tuple3<String, Integer, Integer> right, Context ctx, Collector<String> out) throws Exception {
                                // 进入这个方法,是关联上的数据
                                out.collect(left + "<------>" + right);
                            }
                        })
                .print();
        env.execute();
    }
}

2.处理迟到数据

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        SingleOutputStreamOperator<WaterSensor> stream1 = env.socketTextStream("hadoop102", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs();
                            }
                        }));

        SingleOutputStreamOperator<Tuple2<String, Integer>> stream2 = env.socketTextStream("hadoop102", 8888)
                .map(new MapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> map(String value) throws Exception {
                        String[] split = value.split(",");
                        return Tuple2.of(split[0], Integer.valueOf(split[1]));
                    }
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Integer>>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Integer>>() {
                            @Override
                            public long extractTimestamp(Tuple2<String, Integer> element, long recordTimestamp) {
                                return element.f1;
                            }
                        }));

        SingleOutputStreamOperator<String> process = stream1.keyBy(WaterSensor::getId)
                .intervalJoin(stream2.keyBy(value -> value.f0))
                .between(Time.milliseconds(-3), Time.milliseconds(3))
                .process(new ProcessJoinFunction<WaterSensor, Tuple2<String, Integer>, String>() {
                    @Override
                    public void processElement(WaterSensor left, Tuple2<String, Integer> right, ProcessJoinFunction<WaterSensor, Tuple2<String, Integer>, String>.Context ctx, Collector<String> out) throws Exception {
                        out.collect(left.toString() + "," + right.toString());
                    }
                });

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

6.处理函数

1.处理函数的分类

Flink提供了8个不同的处理函数:

(1)ProcessFunction

最基本的处理函数,基于DataStream直接调用.process()时作为参数传入。

(2)KeyedProcessFunction

对流按键分区后的处理函数,基于KeyedStream调用.process()时作为参数传入。要想使用定时器,比如基于KeyedStream。

(3)ProcessWindowFunction

开窗之后的处理函数,也是全窗口函数的代表。基于WindowedStream调用.process()时作为参数传入。

(4)ProcessAllWindowFunction

同样是开窗之后的处理函数,基于AllWindowedStream调用.process()时作为参数传入。

(5)CoProcessFunction

合并(connect)两条流之后的处理函数,基于ConnectedStreams调用.process()时作为参数传入。关于流的连接合并操作,我们会在后续章节详细介绍。

(6)ProcessJoinFunction

间隔连接(interval join)两条流之后的处理函数,基于IntervalJoined调用.process()时作为参数传入。

(7)BroadcastProcessFunction

广播连接流处理函数,基于BroadcastConnectedStream调用.process()时作为参数传入。这里的“广播连接流”BroadcastConnectedStream,是一个未keyBy的普通DataStream与一个广播流(BroadcastStream)做连接(conncet)之后的产物。关于广播流的相关操作,我们会在后续章节详细介绍。

(8)KeyedBroadcastProcessFunction

按键分区的广播连接流处理函数,同样是基于BroadcastConnectedStream调用.process()时作为参数传入。与BroadcastProcessFunction不同的是,这时的广播连接流,是一个KeyedStream与广播流(BroadcastStream)做连接之后的产物。

2.定时器和定时任务

案例:

public class KeyedProcessTimerDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.enableCheckpointing(2000L, CheckpointingMode.EXACTLY_ONCE);
        KeyedStream<WaterSensor, String> stream = env.socketTextStream("hadoop102", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs()*1000L;
                            }
                        }))
                .keyBy(WaterSensor::getId);
        stream.process(new KeyedProcessFunction<String, WaterSensor, String>() {

            @Override
            public void processElement(WaterSensor value, KeyedProcessFunction<String, WaterSensor, String>.Context ctx, Collector<String> out) throws Exception {
//                获取当前数据的key
                String currentKey = ctx.getCurrentKey();
                System.out.println("key的信息:"+currentKey);
//                1.定时器注册
                TimerService timerService = ctx.timerService();

//                注册定时器: 处理时间
//                timerService.registerProcessingTimeTimer();
//                删除定时器: 事件时间
//                timerService.deleteEventTimeTimer();
//                删除定时器:  处理时间
//                timerService.deleteProcessingTimeTimer();
//                获取当前处理时间:系统时间
                timerService.currentProcessingTime();
//                获取当前的  watermark
                timerService.currentWatermark();
//
                Long timestamp = ctx.timestamp();   //数据中提取出来的事件时间
                timerService.registerEventTimeTimer(5000L);
                System.out.println("当前key=" + currentKey + ",当前时间=" + timestamp + ",注册了一个5s的定时器");
                
            }

            @Override
            public void onTimer(long timestamp, KeyedProcessFunction<String, WaterSensor, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
                super.onTimer(timestamp, ctx, out);
                String currentKey = ctx.getCurrentKey();
                System.out.println("key=" + currentKey + "现在时间是" + timestamp + "定时器触发");
            }
        });
        
        env.execute();


    }
}

注意:TimerService会以键(key)和时间戳为标准,对定时器进行去重;也就是说对于每个key和时间戳,最多只有一个定时器,如果注册了多次,onTimer()方法也将只被调用一次(key 相同,key不同的话就会执行多次)

3.案例

需求:开全窗口,设置滑动窗口,10s一个窗口,5s的滑动步长,统计在10秒内vc最多的个数

1.使用全窗口:

public class ProcessAllWindowTopNDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        SingleOutputStreamOperator<WaterSensor> stream1 = env.socketTextStream("hadoop102", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs() * 1000L;
                            }
                        }));
        stream1.print();
        AllWindowedStream<WaterSensor, TimeWindow> windowAll = stream1.windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)));
        SingleOutputStreamOperator<String> process = windowAll.process(new MyprocessAll());
        process.print();
        env.execute();

    }
    public static class MyprocessAll extends ProcessAllWindowFunction<WaterSensor,String,TimeWindow>{

        @Override
        public void process(ProcessAllWindowFunction<WaterSensor, String, TimeWindow>.Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
            Map<Integer, Integer> map = new HashMap<>();
            for (WaterSensor element : elements) {
                Integer vc = element.getVc();
                if(map.containsKey(vc)){
                    map.put(vc,map.get(vc)+1);
                }else {
                    map.put(vc,1);
                }
            }

            List<Tuple2<Integer, Integer>> list = new ArrayList<>();

            for (Integer integer : map.keySet()) {
                list.add(Tuple2.of(integer,map.get(integer)));
            }

            list.sort(new Comparator<Tuple2<Integer, Integer>>() {
                @Override
                public int compare(Tuple2<Integer, Integer> o1, Tuple2<Integer, Integer> o2) {
                    return o2.f1 - o1.f1;
                }
            });

            StringBuffer buffer = new StringBuffer();

            for (int i = 0; i < Math.min(2,list.size()); i++) {
                Tuple2<Integer, Integer> integerIntegerTuple2 = list.get(i);
                String start = DateFormatUtils.format(context.window().getStart(), "yyyy-MM-dd HH:ss:mm");
                String end = DateFormatUtils.format(context.window().getEnd(), "yyyy-MM-dd HH:ss:mm");
                buffer.append("窗口:[" + start + "," + end + ")");
                buffer.append("\n");
                buffer.append("第"+ (i+1) +"名:vc=" +  integerIntegerTuple2.f0 + "  数量:" + integerIntegerTuple2.f1);
                buffer.append("\n");
            }

            out.collect(buffer.toString());

        }
    }
}

2.先keyby在计算

public class KeyedProcessFunctionTopNDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        KeyedStream<WaterSensor, Integer> keyStream = env.socketTextStream("hadoop102", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs() * 1000L;
                            }
                        }))
                .keyBy(WaterSensor::getVc);

        keyStream.print();



        WindowedStream<WaterSensor, Integer, TimeWindow> windowStream = keyStream.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)));
        SingleOutputStreamOperator<Tuple3<Integer, Integer, Long>> aggregate = windowStream.aggregate(new AggregateFunction<WaterSensor, Integer, Integer>() {
            @Override
            public Integer createAccumulator() {
                return 0;
            }

            @Override
            public Integer add(WaterSensor value, Integer accumulator) {
                return 1 + accumulator;
            }

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

            @Override
            public Integer merge(Integer a, Integer b) {
                return null;
            }
        }, new WindowFunction<Integer, Tuple3<Integer, Integer, Long>, Integer, TimeWindow>() {
            @Override
            public void apply(Integer integer, TimeWindow window, Iterable<Integer> input, Collector<Tuple3<Integer, Integer, Long>> out) throws Exception {

                out.collect(Tuple3.of(integer, input.iterator().next(), window.getStart()));
            }
        });

        KeyedStream<Tuple3<Integer, Integer, Long>, Long> keyByStream = aggregate.keyBy(value -> value.f2);

        SingleOutputStreamOperator<String> process = keyByStream.process(new KeyedProcessFunction<Long, Tuple3<Integer, Integer, Long>, String>() {
            List<Tuple3<Integer, Integer, Long>> list = new ArrayList<>();

            @Override
            public void processElement(Tuple3<Integer, Integer, Long> value, KeyedProcessFunction<Long, Tuple3<Integer, Integer, Long>, String>.Context ctx, Collector<String> out) throws Exception {
                String winkey = DateFormatUtils.format(ctx.getCurrentKey(), "yyyy-MM-dd HH:mm:ss");
                
                list.add(value);
                
                list.sort(new Comparator<Tuple3<Integer, Integer, Long>>() {
                    @Override
                    public int compare(Tuple3<Integer, Integer, Long> o1, Tuple3<Integer, Integer, Long> o2) {
                        return o2.f1 - o1.f1;
                    }
                });
                
//                System.out.println(list.toString());
                StringBuffer buffer = new StringBuffer();

                for (int i = 0; i < Math.min(2,list.size()); i++) {
                    buffer.append("窗口大小:" + winkey.toString());
                    buffer.append(",key值" + list.get(i).f0.toString() + ",排名"+(i+1)+":值为:" + list.get(i).f1.toString());
                    buffer.append("\n");
                    buffer.append("-----------------------------------------------");
                    buffer.append("\n");

                }

                out.collect(buffer.toString());

            }
        });

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

4.测输出流

需求:对每个传感器,vc超过10的输出告警信息

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

        OutputTag<String> warn = new OutputTag<>("warn", Types.STRING);

        SingleOutputStreamOperator<WaterSensor> stream = env.socketTextStream("hadoop102", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs() * 1000L;
                            }
                        }))
                .keyBy(WaterSensor::getVc)
                .process(new KeyedProcessFunction<Integer, WaterSensor, WaterSensor>() {
                    @Override
                    public void processElement(WaterSensor value, KeyedProcessFunction<Integer, WaterSensor, WaterSensor>.Context ctx, Collector<WaterSensor> out) throws Exception {
                        if (value.getVc() > 10) {
                            ctx.output(warn, "当前水位=" + value.getVc() + ",大于阈值10!!!");
                        }

                        out.collect(value);
                    }
                });

        stream.print("正常流:");
        stream.getSideOutput(warn).printToErr("warn");

        env.execute();
    }
}

7.状态管理

1.状态分类
2.按键分区状态
1.值状态(ValueState)

值状态相当与java中的基本类型,存储的就是一个值

T value():获取当前状态的值;

update(T value):对状态进行更新,传入的参数value就是要覆写的状态值。

需求:检测每种传感器的水位值(vc),如果连续的两个水位值超过10,就输出报警。

public class KeyedListStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

//        env.fromSource(KafkaSource.builder().build(), WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)),"kafkasource");
        KeyedStream<WaterSensor, String> stream = env.socketTextStream("1.94.41.70", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs();
                            }
                        }))
                .keyBy(WaterSensor::getId);

        stream.process(new KeyedProcessFunction<String, WaterSensor, String>() {

            ValueState<Integer> lastValue;

            @Override
            public void open(Configuration parameters) throws Exception {
                super.open(parameters);
                lastValue = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("lastVcState", Types.INT));
            }

            @Override
            public void processElement(WaterSensor value, KeyedProcessFunction<String, WaterSensor, String>.Context ctx, Collector<String> out) throws Exception {
                Integer value1 = lastValue.value() == null ? 0 : lastValue.value();
                Integer vc = value.getVc();
                if(Math.abs(vc-value1) > 10 ){
                    out.collect("传感器=" + value.getId() + "==>当前水位值=" + vc + ",与上一条水位值=" + value1 + ",相差超过10!!!!");
                }

                lastValue.update(vc);
            }
        })
                        .print();

        env.execute();
    }
}
2.列表状态(ListState)

Iterable get():获取当前的列表状态,返回的是一个可迭代类型Iterable;

update(List values):传入一个列表values,直接对状态进行覆盖;

add(T value):在状态列表中添加一个元素value;

addAll(List values):向列表中添加多个元素,以列表values形式传入。

需求:针对每种传感器(vc)输出最高的3个水位值

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        env.socketTextStream("1.94.41.70",7777)
                        .map(value -> {
                            String[] split = value.split(",");
                            return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
                        })
                                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3L))
                                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                                            @Override
                                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                                return element.getTs();
                                            }
                                        }))
                                        .keyBy(WaterSensor::getId)
                                                .process(new KeyedProcessFunction<String, WaterSensor, String>() {
                                                    ListState<Integer> vcList ;

                                                    @Override
                                                    public void open(Configuration parameters) throws Exception {
                                                         vcList = getRuntimeContext().getListState(new ListStateDescriptor<Integer>("vcList", Types.INT));
                                                    }

                                                    @Override
                                                    public void processElement(WaterSensor value, KeyedProcessFunction<String, WaterSensor, String>.Context ctx, Collector<String> out) throws Exception {
                                                        vcList.add(value.getVc());
                                                        List<Integer> List = new ArrayList<>();
                                                        for (Integer vc : vcList.get()) {
                                                            List.add(vc);

                                                        }
                                                        List.sort(new Comparator<Integer>() {
                                                            @Override
                                                            public int compare(Integer o1, Integer o2) {
                                                                return o2 - o1;
                                                            }
                                                        });

                                                        if(List.size() > 3){
                                                            for (int i = 3; i < List.size(); i++) {
                                                                List.remove(i);
                                                            }
                                                        }
                                                        vcList.update(List);

                                                        out.collect("传感器id为" + value.getId() + ",最大的3个水位值=" + List.toString());
                                                    }
                                                })
                                                        .print();


        env.execute();
    }
}
3.Map状态(MapState)

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 keys():获取映射状态中所有的键(key),返回一个可迭代Iterable类型;

Iterable values():获取映射状态中所有的值(value),返回一个可迭代Iterable类型;

boolean isEmpty():判断映射是否为空,返回一个boolean值。

案例:统计每种传感器(vc)每种水位值出现的次数。

public class KeyedMapStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        env.socketTextStream("1.94.41.70",7777)
                        .map(value -> {
                            String[] split = value.split(",");
                            return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
                        })
                                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                                            @Override
                                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                                return element.getTs();
                                            }
                                        }))
                                        .keyBy(value -> value.id)
                                                .process(new KeyedProcessFunction<String, WaterSensor, String>() {
                                                    MapState<Integer, Integer> mapState;

                                                    @Override
                                                    public void open(Configuration parameters) throws Exception {
                                                        mapState = getRuntimeContext().getMapState(new MapStateDescriptor<Integer, Integer>("mapState", Types.INT, Types.INT));
                                                    }

                                                    @Override
                                                    public void processElement(WaterSensor value, KeyedProcessFunction<String, WaterSensor, String>.Context ctx, Collector<String> out) throws Exception {
                                                        if(mapState.contains(value.getVc())){
                                                            mapState.put(value.getVc(),mapState.get(value.getVc())+1) ;
                                                        }else {
                                                            mapState.put(value.getVc(),1);
                                                        }
                                                        StringBuffer stringBuffer = new StringBuffer();
                                                        stringBuffer.append("======================================");
                                                        stringBuffer.append("\n");
                                                        stringBuffer.append("传感器id为" + value.getId() + "\n");
                                                        for (Map.Entry<Integer, Integer> entry : mapState.entries()) {
                                                            stringBuffer.append(entry.toString());
                                                            
                                                        }
                                                        out.collect(stringBuffer.toString());


                                                    }
                                                })
                                                        .print();


        env.execute();
    }
}
4.归约状态(ReducingState)

需求:计算每种传感器(vc)的水位和

public class keyedReducingStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        env.socketTextStream("1.94.41.70",7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs();
                            }
                        }))
                .keyBy(value -> value.id)
                .process(new KeyedProcessFunction<String, WaterSensor, String>() {
                             ReducingState<Integer> reducing;

                             @Override
                             public void open(Configuration parameters) throws Exception {
                                  reducing = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Integer>("reducing", new ReduceFunction<Integer>() {
                                     @Override
                                     public Integer reduce(Integer value1, Integer value2) throws Exception {
                                         return value1 + value2;
                                     }
                                 }, Types.INT));
                             }

                             @Override
                             public void processElement(WaterSensor value, KeyedProcessFunction<String, WaterSensor, String>.Context ctx, Collector<String> out) throws Exception {
                                 reducing.add(value.getVc());



                                 out.collect("key的值:"+value.id +",vc"+"的总和"+reducing.get().toString());
                             }
                         }
                ).print();
        
        env.execute();
    }
}
5.聚合状态(AggregatingState)

需求:计算每种传感器的平均水位

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

        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        env.socketTextStream("1.94.41.70",7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3L))
                        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
                            @Override
                            public long extractTimestamp(WaterSensor element, long recordTimestamp) {
                                return element.getTs();
                            }
                        }))
                .keyBy(WaterSensor::getId)
                .process(new KeyedProcessFunction<String, WaterSensor, String>() {
                    AggregatingState<Integer, Double> aggState;

                    @Override
                    public void open(Configuration parameters) throws Exception {
                         aggState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Integer, Tuple2<Integer, Integer>, Double>("aggState", new AggregateFunction<Integer, Tuple2<Integer, Integer>, Double>() {
                            @Override
                            public Tuple2<Integer, Integer> createAccumulator() {
                                return Tuple2.of(0, 0);
                            }

                            @Override
                            public Tuple2<Integer, Integer> add(Integer value, Tuple2<Integer, Integer> accumulator) {
                                return Tuple2.of(accumulator.f0 + value, accumulator.f1 + 1);
                            }

                            @Override
                            public Double getResult(Tuple2<Integer, Integer> accumulator) {
                                return accumulator.f0 * 1D / accumulator.f1;
                            }

                            @Override
                            public Tuple2<Integer, Integer> merge(Tuple2<Integer, Integer> a, Tuple2<Integer, Integer> b) {
                                return null;
                            }
                        }, Types.TUPLE(Types.INT, Types.INT)));
                    }

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

                        aggState.add(value.getVc());
                        out.collect("key为:"+value.id+",vc的平均值为:"+aggState.get().toString());
                    }
                })
                .print();

        env.execute();

    }
}
3.算子状态

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

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

1.列表状态(ListState)

案例:在map算子中计算数据的个数。

public class OperatorListStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);

//        env.fromSource(KafkaSource.builder().build(), WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)),"kafkasource");
        DataStreamSource<String> source = env.socketTextStream("1.94.41.70", 7777);
        SingleOutputStreamOperator<Long> map = source.map(new MyCountMapFunction());
        map.print();
        env.execute();

    }


    private static class MyCountMapFunction implements MapFunction<String,Long> , CheckpointedFunction {
        Long count = 0L;
        ListState<Long> state;

        @Override
        public Long map(String value) throws Exception {

            return count++;
        }

        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {
            System.out.println("snapshotState....");
            state.clear();
            state.add(count);

        }

        /**
         * 初始化方法
         * @param context
         * @throws Exception
         */
        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            System.out.println("initializeState......");
            state = context.getOperatorStateStore().getListState(new ListStateDescriptor<Long>("name", Types.LONG));

            if (context.isRestored()) {
                for (Long c : state.get()) {
                    count += c;
                }
            }

        }
    }
}

2.联合列表状态(UnionListState)

算子状态中, list 与 unionlist的区别:并行度改变时,怎么重新分配状态

1、List状态:轮询均分 给 新的 并行子任务

2、unionlist状态: 原先的多个子任务的状态,合并成一份完整的。 给新的并行子任务 ,每人一份完整的

3.广播状态(BroadcastState)

需求:水位(DataDS流)超过指定的阈值发送告警,阈值可以动态修改(Config流)。

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

        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        SingleOutputStreamOperator<WaterSensor> DataDS = env.socketTextStream("1.94.41.70", 7777)
                .map(value -> {
                    String[] split = value.split(",");
                    return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
                });

        DataStreamSource<String> ConfigDS = env.socketTextStream("1.94.41.70", 8888);
        MapStateDescriptor<String, Integer> broad = new MapStateDescriptor<>("broad", Types.STRING, Types.INT);
        BroadcastStream<String> broadcast = ConfigDS.broadcast(broad);

        BroadcastConnectedStream<WaterSensor, String> connect = DataDS.connect(broadcast);
        connect.process(new BroadcastProcessFunction<WaterSensor, String, String>() {
            @Override
            public void processElement(WaterSensor value, BroadcastProcessFunction<WaterSensor, String, String>.ReadOnlyContext ctx, Collector<String> out) throws Exception {
                ReadOnlyBroadcastState<String, Integer> broadcastState = ctx.getBroadcastState(broad);
                Integer broad1 = broadcastState.get("broad");
                Integer vc = value.getVc();
                int broad2 = broad1 == null ? 0 : broad1;
                if (vc > broad2){
                    out.collect(value + ",水位超过指定的阈值:" + broad2 + "!!!");
                }

            }

            @Override
            public void processBroadcastElement(String value, BroadcastProcessFunction<WaterSensor, String, String>.Context ctx, Collector<String> out) throws Exception {
                BroadcastState<String, Integer> broadcastState = ctx.getBroadcastState(broad);
                broadcastState.put("broad",Integer.valueOf(value));

            }
        })

                .print();
        env.execute();


    }
}
4.状态后端

1.负责管理 本地状态 2.hashmap

存在 TM的 JVM的堆内存,读写快,缺点是存不了太多(受限与TaskManager的内存)

rocksdb

存在 TM所在节点的rocksdb数据库,存到磁盘中,写–序列化,读–反序列化读写相对慢一些,可以存很大的状态 3.配置方式 ​ 1)配置文件 默认值 ink-conf.yaml

2)代码中指定

hashmap

env.setStateBackend(new HashMapStateBackend());

rocksdb

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-statebackend-rocksdb</artifactId>
            <version>${flink.version}</version>
        </dependency>
env.setStateBackend(new EmbeddedRocksDBStateBackend());

3)提交参数指定

flink run-application -t yarn-application
		-p3
		-Dstate.backend.type=rocksdb
		-c 全类名-
		jar包

8.容错机制

1.检查点

在流处理中,我们可以用存档读档的思路,就是将之前某个时间点所有的状态保存下来,这份“存档”就是所谓的“检查点”(checkpoint)。

1.检查点算法
分布式快照算法
1.Barrier对齐

精准一次

至少一次

2.Barrier不对齐

总结:

1.Barrier对齐: 一个Task 收到|所有上游 同一个编号的 barrier之后,才会对自己的本地状态做 备份

精准一次: 在barrier对齐过程中,barrier后面的数据 阻塞等待(不会越过barrier)

至少一次: 在barrier对齐过程中,先到的barrier,其后面的数据 不阻塞 接者计算 2.非Barrier对齐:一个Task 收到 第一个 barrier时,就开始 执行备份,能保证 准一次(fLink 1.11出的新法)

先到的barrier,将 本地状态 备份, 其后面的数据接者计算输出

未到的barrier,其前面的数据 接着计算输出,同时 也保存到备份中

最后一个barrier到达 该Task时,这个Task的备份结束

案例:

borrier对齐

//        1.启动检查点:默认是borrier对齐的,周期为5s,精确一次
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        CheckpointConfig checkpointConfig = env.getCheckpointConfig();
//        2.指定检查点的存储位置
        checkpointConfig.setCheckpointStorage("htfs://hadoop102:8020/chk");
//        3.checkpoint的超时时间:默认10分钟
        checkpointConfig.setCheckpointTimeout(60000);
//        4.同时运行中的checkpoint的最大数量,默认是1,一般都是1
        checkpointConfig.setMaxConcurrentCheckpoints(1);
//        5.最小等待间隔:上一轮checkpoint结束  到  下一轮checkpoint开始  之间的间隔,设置>0,并发就会变成1
        checkpointConfig.setMinPauseBetweenCheckpoints(1000);
//        6.取消作业时,checkpoint的数据,是否保留在外部系统
//          DELETE_ON_CANCELLATION:动cancel时,删除存在外部系统的chk-xx目录(如果是程序笑然挂掉,不会删)
        checkpointConfig.setExternalizedCheckpointCleanup(CheckpointConfig.ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION);
//        7.允许checkpint  连续失败的次数,默认 0---> 表示checkpoint一失败,job就挂掉
        checkpointConfig.setTolerableCheckpointFailureNumber(10);

因为存储到hadoop中,所以添加hadoop依赖,但是打包不要打在包中

        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>3.3.4</version>
            <scope>provided</scope>
        </dependency>

borrier非对齐:

//        1.启动检查点:默认是borrier对齐的,周期为5s,精确一次
        env.enableCheckpointing(2000, CheckpointingMode.EXACTLY_ONCE);
        CheckpointConfig checkpointConfig = env.getCheckpointConfig();
//        2.指定检查点的存储位置
        checkpointConfig.setCheckpointStorage("htfs://hadoop102:8020/chk");
//        3.checkpoint的超时时间:默认10分钟
        checkpointConfig.setCheckpointTimeout(60000);
//        4.同时运行中的checkpoint的最大数量,默认是1,一般都是1
        checkpointConfig.setMaxConcurrentCheckpoints(1);
//        5.最小等待间隔:上一轮checkpoint结束  到  下一轮checkpoint开始  之间的间隔,设置>0,并发就会变成1
        checkpointConfig.setMinPauseBetweenCheckpoints(1000);
//        6.取消作业时,checkpoint的数据,是否保留在外部系统
//          DELETE_ON_CANCELLATION:动cancel时,删除存在外部系统的chk-xx目录(如果是程序笑然挂掉,不会删)
        checkpointConfig.setExternalizedCheckpointCleanup(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
//        7.允许checkpint  连续失败的次数,默认 0---> 表示checkpoint一失败,job就挂掉
        checkpointConfig.setTolerableCheckpointFailureNumber(10);

//        TODO 开启  非对齐检查点(barrier非对齐)
//        开启的要求:checkpoint模式必须是精确一次,最大并发必须设为1
        checkpointConfig.enableUnalignedCheckpoints();
//        开启非对齐检查点才生效:  默认0, 表示一开始就直接用   非对齐的检查点
//        如果大于0,一开始用  对齐的检查点(barrier对齐),对齐的时间超过这个参数,自动切换成   非对齐检查点  (barrier非对齐)
        checkpointConfig.setAlignedCheckpointTimeout(Duration.ofSeconds(1));

2.通用增量 checkpoint (changelog)(了解)

从 1.15 开始,不管hashmap还是rocksdb 状态后端都可以通过开启changelog实现通用的增量checkpoint。

注意事项

(1)目前标记为实验性功能,开启后可能会造成资源消耗增大:

HDFS上保存的文件数变多

消耗更多的IO带宽用于上传变更日志

更多的CPU用于序列化状态更改

TaskManager使用更多内存来缓存状态更改

(2)使用限制:

Checkpoint的最大并发必须为1

从 Flink 1.15 开始,只有文件系统的存储类型实现可用(memory测试阶段)

不支持 NO_CLAIM 模式

使用方式

(1)方式一:配置文件指定

state.backend.changelog.enabled: true

state.backend.changelog.storage: filesystem 

\# 存储 changelog 数据

dstl.dfs.base-path: hdfs://hadoop102:8020/changelog 

execution.checkpointing.max-concurrent-checkpoints: 1

execution.savepoint-restore-mode: CLAIM

(2)方式二:在代码中设置

需要引入依赖:

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-statebackend-changelog</artifactId>
  <version>${flink.version}</version>
  <scope>runtime</scope>
</dependency>

开启changelog:

env.enableChangelogStateBackend(true);
3.最终检查点(开启,不要关闭)(默认是开启的)
Configuration config = new Configuration();
config.set(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH, true);
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
2.保存点

保存点与检查点最大的区别,就是触发的时机。检查点是由Flink自动管理的,定期创建,发生故障之后自动读取进行恢复,这是一个“自动存盘”的功能;而保存点不会自动创建,必须由用户明确地手动触发保存操作,所以就是“手动存盘”。

一句话理解:检查点是flink自己定时保存,保存点是我们自己使用命令保存

建议:(生产环境也要写的)

对于没有设置ID的算子,Flink默认会自动进行设置,所以在重新启动应用后可能会导致ID不同而无法兼容以前的状态。所以为了方便后续的维护,强烈建议在程序中为每一个算子手动指定ID。

DataStream<String> stream = env
    .addSource(new StatefulSource()).uid("source-id")
    .map(new StatefulMapper()).uid("mapper-id")
    .print();
1.使用保存点

保存点的使用非常简单,我们可以使用命令行工具来创建保存点,也可以从保存点恢复作业。

(1)创建保存点

要在命令行中为运行的作业创建一个保存点镜像,只需要执行:

bin/flink savepoint :jobId [:targetDirectory]

这里jobId需要填充要做镜像保存的作业ID,目标路径targetDirectory可选,表示保存点存储的路径。

对于保存点的默认路径,可以通过配置文件flink-conf.yaml中的state.savepoints.dir项来设定:

state.savepoints.dir: hdfs:///flink/savepoints

当然对于单独的作业,我们也可以在程序代码中通过执行环境来设置:

env.setDefaultSavepointDir("hdfs:///flink/savepoints");

由于创建保存点一般都是希望更改环境之后重启,所以创建之后往往紧接着就是停掉作业的操作。除了对运行的作业创建保存点,我们也可以在停掉一个作业时直接创建保存点:

bin/flink stop --savepointPath [:targetDirectory] :jobId

(2)从保存点重启应用

我们已经知道,提交启动一个Flink作业,使用的命令是flink run;现在要从保存点重启一个应用,其实本质是一样的:

bin/flink run -s :savepointPath [:runArgs]

这里只要增加一个-s参数,指定保存点的路径就可以了,其它启动时的参数还是完全一样的,如果是基于yarn的运行模式还需要加上 -yid application-id。我们在第三章使用web UI进行作业提交时,可以填入的参数除了入口类、并行度和运行参数,还有一个“Savepoint Path”,这就是从保存点启动应用的配置。

2.使用保存点切换状态后端

使用savepoint恢复状态的时候,也可以更换状态后端。但是有一点需要注意的是,不要在代码中指定状态后端了, 通过配置文件来配置或者-D 参数配置。

打包时,服务器上有的就provided,可能遇到依赖问题,报错:javax.annotation.Nullable找不到,可以导入如下依赖:

    <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>jsr305</artifactId>
      <version>1.3.9</version>
    </dependency>

(1)提交flink作业

bin/flink run-application -d -t yarn-application -Dstate.backend=hashmap -c com.atguigu.checkpoint.SavepointDemo FlinkTutorial-1.0-SNAPSHOT.jar

(2)停止flink作业时,触发保存点

方式一:stop优雅停止并触发保存点,要求source实现StoppableFunction接口

bin/flink stop -p savepoint 路径 job-id -yid application-id

方式二:cancel立即停止并触发保存点

bin/flink cancel -s savepoint路径 job-id -yid application-id

案例中source是socket,不能用stop

bin/flink cancel -s hdfs://hadoop102:8020/sp cffca338509ea04f38f03b4b77c8075c -yid application_1681871196375_0001

(3)从savepoint恢复作业,同时修改状态后端

bin/flink run-application -d -t yarn-application -s hdfs://hadoop102:8020/sp/savepoint-267cc0-47a214b019d5 -Dstate.backend=rocksdb -c com.atguigu.checkpoint.SavepointDemo FlinkTutorial-1.0-SNAPSHOT.jar 

(4)从保存下来的checkpoint恢复作业

bin/flink run-application -d -t yarn-application -Dstate.backend=rocksdb -s hdfs://hadoop102:8020/chk/532f87ef4146b2a2968a1c137d33d4a6/chk-175 -c com.atguigu.checkpoint.SavepointDemo ./FlinkTutorial-1.0-SNAPSHOT.jar

如果停止作业时,忘了触发保存点也不用担心,现在版本的flink支持从保留在外部系统的checkpoint恢复作业,但是恢复时不支持切换状态后端。

3.端到端精确一次

9.Flink SQL

注意:flinksql与hivesql很多类似,这里就写我不熟悉和hivesql不一致的地方

1.SQL-client准备
1.基于yarn-session模式

1.启动flink

前提:先启动hadoop集群

/opt/module/flink-1.17.0/bin/yarn-session.sh -d

2.启动Flink的sqlclient

/opt/module/flink-1.17.0/bin/sql-client.sh embedded -s yarn-session
2.常见配置

1)结果显示模式

#默认table,还可以设置为tableau、changelog
SET sql-client.execution.result-mode=tableau;

3)执行环境

SET execution.runtime-mode=streaming; #默认streaming,也可以设置batch

4)默认并行度

SET parallelism.default=1;

5)设置状态TTL

SET table.exec.state.ttl=1000;

6)通过sql文件初始化

(1)创建sql文件

vim conf/sql-client-init.sql

SET sql-client.execution.result-mode=tableau;
CREATE DATABASE mydatabase;

(2)启动时,指定sql文件

/opt/module/flink-1.17.0/bin/sql-client.sh embedded -s yarn-session -i conf/sql-client-init.sql
2.处理时间
1.事件时间

WATERMARK FOR ts AS ts - INTERVAL ‘5’ SECOND

CREATE TABLE EventTable(
  user STRING,
  url STRING,
  ts TIMESTAMP(3),
  WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
  ...
);
2.处理时间

ts AS PROCTIME()

CREATE TABLE EventTable(
  user STRING,
  url STRING,
  ts AS PROCTIME()
) WITH (
  ...
);
3.DDL
1.数据库

1)创建数据库

(1)语法

CREATE DATABASE [IF NOT EXISTS] [catalog_name.]db_name
 [COMMENT database_comment]
 WITH (key1=val1, key2=val2, ...)

(2)案例

CREATE DATABASE db_flink;

2查询数据库

(1)查询所有数据库

SHOW DATABASES

(2)查询当前数据库

SHOW CURRENT DATABASE

3)修改数据库

ALTER DATABASE [catalog_name.]db_name SET (key1=val1, key2=val2, ...)

4)删除数据库

DROP DATABASE [IF EXISTS] [catalog_name.]db_name [ (RESTRICT | CASCADE) ]

RESTRICT:删除非空数据库会触发异常。默认启用

CASCADE:删除非空数据库也会删除所有相关的表和函数。

DROP DATABASE db_flink2;

5)切换当前数据库

USE database_name;
2.表

1)创建表

(1)语法

CREATE TABLE [IF NOT EXISTS] [catalog_name.][db_name.]table_name
 (
  { <physical_column_definition> | <metadata_column_definition> | <computed_column_definition> }[ , ...n]
  [ <watermark_definition> ]
  [ <table_constraint> ][ , ...n]
 )
 [COMMENT table_comment]
 [PARTITIONED BY (partition_column_name1, partition_column_name2, ...)]
 WITH (key1=val1, key2=val2, ...)
 [ LIKE source_table [( <like_options> )] | AS select_query ]

① physical_column_definition

物理列是数据库中所说的常规列。其定义了物理介质中存储的数据中字段的名称、类型和顺序。其他类型的列可以在物理列之间声明,但不会影响最终的物理列的读取。

② metadata_column_definition

元数据列是 SQL 标准的扩展,允许访问数据源本身具有的一些元数据。元数据列由 METADATA 关键字标识。例如,我们可以使用元数据列从Kafka记录中读取和写入时间戳,用于基于时间的操作(这个时间戳不是数据中的某个时间戳字段,而是数据写入 Kafka 时,Kafka 引擎给这条数据打上的时间戳标记)。connector和format文档列出了每个组件可用的元数据字段。

CREATE TABLE MyTable (
 `user_id` BIGINT,
 `name` STRING,
 `record_time` TIMESTAMP_LTZ(3) METADATA FROM 'timestamp'
) WITH (
 'connector' = 'kafka'
 ...
);

如果自定义的列名称和 Connector 中定义 metadata 字段的名称一样, FROM xxx 子句可省略

CREATE TABLE MyTable (
`user_id` BIGINT,
`name` STRING,
`timestamp` TIMESTAMP_LTZ(3) METADATA
) WITH (
'connector' = 'kafka'
...
);

如果自定义列的数据类型和 Connector 中定义的 metadata 字段的数据类型不一致,程序运行时会自动 cast强转,但是这要求两种数据类型是可以强转的。

CREATE TABLE MyTable (
`user_id` BIGINT,
`name` STRING,
-- 将时间戳强转为 BIGINT
`timestamp` BIGINT METADATA
) WITH (
'connector' = 'kafka'
...
);

默认情况下,Flink SQL planner 认为 metadata 列可以读取和写入。然而,在许多情况下,外部系统提供的只读元数据字段比可写字段多。因此,可以使用VIRTUAL关键字排除元数据列的持久化(表示只读)。

CREATE TABLE MyTable (
 `timestamp` BIGINT METADATA, 
 `offset` BIGINT METADATA VIRTUAL,
 `user_id` BIGINT,
 `name` STRING,
) WITH (
 'connector' = 'kafka'
 ...
);

③ computed_column_definition

计算列是使用语法column_name AS computed_column_expression生成的虚拟列。

计算列就是拿已有的一些列经过一些自定义的运算生成的新列,在物理上并不存储在表中,只能读不能写。列的数据类型从给定的表达式自动派生,无需手动声明。

CREATE TABLE MyTable (
 `user_id` BIGINT,
 `price` DOUBLE,
 `quantity` DOUBLE,
 `cost` AS price * quanitity
) WITH (
 'connector' = 'kafka'
 ...
);

④ 定义Watermark

Flink SQL 提供了几种 WATERMARK 生产策略:

Ø 严格升序:WATERMARK FOR rowtime_column AS rowtime_column。

Flink 任务认为时间戳只会越来越大,也不存在相等的情况,只要相等或者小于之前的,就认为是迟到的数据。

Ø 递增:WATERMARK FOR rowtime_column AS rowtime_column - INTERVAL ‘0.001’ SECOND 。

一般基本不用这种方式。如果设置此类,则允许有相同的时间戳出现。

Ø 有界无序: WATERMARK FOR rowtime_column AS rowtime_column – INTERVAL ‘string’ timeUnit 。

此类策略就可以用于设置最大乱序时间,假如设置为 WATERMARK FOR rowtime_column AS rowtime_column - INTERVAL ‘5’ SECOND ,则生成的是运行 5s 延迟的Watermark。一般都用这种 Watermark 生成策略,此类 Watermark 生成策略通常用于有数据乱序的场景中,而对应到实际的场景中,数据都是会存在乱序的,所以基本都使用此类策略。

⑤ PRIMARY KEY

主键约束表明表中的一列或一组列是唯一的,并且它们不包含NULL值。主键唯一地标识表中的一行,只支持 not enforced。

CREATE TABLE MyTable (
`user_id` BIGINT,
`name` STRING,
PARYMARY KEY(user_id) not enforced
) WITH (
'connector' = 'kafka'
...
);

⑥ PARTITIONED BY

创建分区表

⑦ with语句

用于创建表的表属性,用于指定外部存储系统的元数据信息。配置属性时,表达式key1=val1的键和值都应该是字符串字面值。如下是Kafka的映射表:

CREATE TABLE KafkaTable (
`user_id` BIGINT,
`name` STRING,
`ts` TIMESTAMP(3) METADATA FROM 'timestamp'
) WITH (
'connector' = 'kafka',
'topic' = 'user_behavior',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'testGroup',
'scan.startup.mode' = 'earliest-offset',
'format' = 'csv'
)

一般 with 中的配置项由 Flink SQL 的 Connector(链接外部存储的连接器) 来定义,每种 Connector 提供的with 配置项都是不同的。

⑧ LIKE

用于基于现有表的定义创建表。此外,用户可以扩展原始表或排除表的某些部分。

可以使用该子句重用(可能还会覆盖)某些连接器属性,或者向外部定义的表添加水印。

CREATE TABLE Orders (
  `user` BIGINT,
  product STRING,
  order_time TIMESTAMP(3)
) WITH ( 
  'connector' = 'kafka',
  'scan.startup.mode' = 'earliest-offset'
);

CREATE TABLE Orders_with_watermark (
  -- Add watermark definition
  WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND 
) WITH (
  -- Overwrite the startup-mode
  'scan.startup.mode' = 'latest-offset'
)
LIKE Orders;

⑨ AS select_statement(CTAS)

在一个create-table-as-select (CTAS)语句中,还可以通过查询的结果创建和填充表。CTAS是使用单个命令创建数据并向表中插入数据的最简单、最快速的方法。

CREATE TABLE my_ctas_table
WITH (
  'connector' = 'kafka',
  ...
)
AS SELECT id, name, age FROM source_table WHERE mod(id, 10) = 0;

注意:CTAS有以下限制:

Ø 暂不支持创建临时表。

Ø 目前还不支持指定显式列。

Ø 还不支持指定显式水印。

Ø 目前还不支持创建分区表。

Ø 目前还不支持指定主键约束。

(2)简单建表示例

CREATE TABLE test(
  id INT, 
  ts BIGINT, 
  vc INT
) WITH (
'connector' = 'print'
);

CREATE TABLE test1 (
  `value` STRING
)

LIKE test;

2)查看表

(1)查看所有表

SHOW TABLES [ ( FROM | IN ) [catalog_name.]database_name ] [ [NOT] LIKE <sql_like_pattern> ]

如果没有指定数据库,则从当前数据库返回表。

LIKE子句中sql pattern的语法与MySQL方言的语法相同:

Ø %匹配任意数量的字符,甚至零字符,%匹配一个’%'字符。

Ø 只匹配一个字符,_只匹配一个’'字符

(2)查看表信息

{ DESCRIBE | DESC } [catalog_name.][db_name.]table_name

3)修改表

(1)修改表名

ALTER TABLE [catalog_name.][db_name.]table_name RENAME TO new_table_name

(2)修改表属性

ALTER TABLE [catalog_name.][db_name.]table_name SET (key1=val1, key2=val2, ...)

4)删除表

DROP [TEMPORARY] TABLE [IF EXISTS] [catalog_name.][db_name.]table_name
4.查询
1.分组窗口聚合

SQL中只支持基于时间的窗口,不支持基于元素个数的窗口。(1.13后都用TVF,没有用这个了)

分组窗口函数描述
TUMBLE(time_attr, interval)定义一个滚动窗口。滚动窗口把行分配到有固定持续时间( interval )的不重叠的连续窗口。比如,5 分钟的滚动窗口以 5 分钟为间隔对行进行分组。滚动窗口可以定义在事件时间(批处理、流处理)或处理时间(流处理)上。
HOP(time_attr, interval, interval)定义一个跳跃的时间窗口(在 Table API 中称为滑动窗口)。滑动窗口有一个固定的持续时间( 第二个 interval 参数 )以及一个滑动的间隔(第一个 interval 参数 )。若滑动间隔小于窗口的持续时间,滑动窗口则会出现重叠;因此,行将会被分配到多个窗口中。比如,一个大小为 15 分组的滑动窗口,其滑动间隔为 5 分钟,将会把每一行数据分配到 3 个 15 分钟的窗口中。滑动窗口可以定义在事件时间(批处理、流处理)或处理时间(流处理)上。
SESSION(time_attr, interval)定义一个会话时间窗口。会话时间窗口没有一个固定的持续时间,但是它们的边界会根据 interval 所定义的不活跃时间所确定;即一个会话时间窗口在定义的间隔时间内没有时间出现,该窗口会被关闭。例如时间窗口的间隔时间是 30 分钟,当其不活跃的时间达到30分钟后,若观测到新的记录,则会启动一个新的会话时间窗口(否则该行数据会被添加到当前的窗口),且若在 30 分钟内没有观测到新纪录,这个窗口将会被关闭。会话时间窗口可以使用事件时间(批处理、流处理)或处理时间(流处理)。

案例:

1) 准备数据

CREATE TABLE ws (
 id INT,
 vc INT,
 pt AS PROCTIME(), --处理时间
 et AS cast(CURRENT_TIMESTAMP as timestamp(3)), --事件时间
 WATERMARK FOR et AS et - INTERVAL '5' SECOND  --watermark
) WITH (
 'connector' = 'datagen',
 'rows-per-second' = '10',
 'fields.id.min' = '1',
 'fields.id.max' = '3',
 'fields.vc.min' = '1',
 'fields.vc.max' = '100'
);

2)滚动窗口示例(时间属性字段,窗口长度)

select  
id,
TUMBLE_START(et, INTERVAL '5' SECOND)  wstart,
TUMBLE_END(et, INTERVAL '5' SECOND)  wend,
sum(vc) sumVc
from ws
group by id, TUMBLE(et, INTERVAL '5' SECOND);

3)滑动窗口(时间属性字段,滑动步长,窗口长度)

select  
id,
HOP_START(pt, INTERVAL '3' SECOND,INTERVAL '5' SECOND)  wstart,
HOP_END(pt, INTERVAL '3' SECOND,INTERVAL '5' SECOND)  wend,
  sum(vc) sumVc
from ws
group by id, HOP(et, INTERVAL '3' SECOND,INTERVAL '5' SECOND);

4)会话窗口(时间属性字段,会话间隔)

select  
id,
SESSION_START(et, INTERVAL '5' SECOND)  wstart,
SESSION_END(et, INTERVAL '5' SECOND)  wend,
sum(vc) sumVc
from ws
group by id, SESSION(et, INTERVAL '5' SECOND);
2.窗口表值函数(TVF)聚合(重点)

对比GroupWindow,TVF窗口更有效和强大。包括:

1.提供更多的性能优化手段

2.支持GroupingSets语法

3.可以在window聚合中使用TopN

4.提供累积窗口

用法:

FROM TABLE(
窗口类型(TABLE 表名, DESCRIPTOR(时间字段),INTERVAL时间…)
)
GROUP BY [window_start,][window_end,] --可选
1.滚动窗口
SELECT 
window_start, 
window_end, 
id , SUM(vc) 
sumVC
FROM TABLE(
  TUMBLE(TABLE ws, DESCRIPTOR(et), INTERVAL '5' SECONDS))
GROUP BY window_start, window_end, id;
2.滑动窗口

要求: 窗口长度=滑动步长的整数倍(底层会优化成多个小滚动窗口)

SELECT window_start, window_end, id , SUM(vc) sumVC
FROM TABLE(
  HOP(TABLE ws, DESCRIPTOR(et), INTERVAL '5' SECONDS , INTERVAL '10' SECONDS))
GROUP BY window_start, window_end, id;
3.累积窗口

举例:以6秒钟为一个周期,没2秒加一次,如0秒到2秒一次结果,0秒到4秒一次结果,0秒到6秒一次结果,6秒到8秒一次结果,6秒到10一次结果。。。。。。

SELECT 
window_start, 
window_end, 
id , 
SUM(vc) sumVC
FROM TABLE(
  CUMULATE(TABLE ws, DESCRIPTOR(et), INTERVAL '2' SECONDS , INTERVAL '6' SECONDS))
GROUP BY window_start, window_end, id;

grouping sets多维分析

SELECT 
window_start, 
window_end, 
id , 
SUM(vc) sumVC
FROM TABLE(
  TUMBLE(TABLE ws, DESCRIPTOR(et), INTERVAL '5' SECONDS))
GROUP BY window_start, window_end,
rollup( (id) )
--  cube( (id) )
--  grouping sets( (id),()  )
;
3.Over聚合
1.语法
SELECT
  agg_func(agg_col) OVER (
    [PARTITION BY col1[, col2, ...]]
    ORDER BY time_col
    range_definition),
  ...
FROM ...

ORDER BY:必须是时间戳列(事件时间、处理时间),只能升序

PARTITION BY:标识了聚合窗口的聚合粒度

range_definition:这个标识聚合窗口的聚合数据范围,在 Flink 中有两种指定数据范围的方式。第一种为按照行数聚合,第二种为按照时间区间聚合

2.案例

1.按照时间区间聚合

统计每个传感器前10秒到现在收到的水位数据条数。

SELECT 
    id, 
    et, 
    vc,
    count(vc) OVER (
        PARTITION BY id
        ORDER BY et
        RANGE BETWEEN INTERVAL '10' SECOND PRECEDING AND CURRENT ROW
  ) AS cnt
FROM ws

或者

SELECT 
    id, 
    et, 
    vc,
count(vc) OVER w AS cnt,
sum(vc) OVER w AS sumVC
FROM ws
WINDOW w AS (
    PARTITION BY id
    ORDER BY et
    RANGE BETWEEN INTERVAL '10' SECOND PRECEDING AND CURRENT ROW
)

2.按照行数聚合

统计每个传感器前5条到现在数据的平均水位

SELECT 
    id, 
    et, 
    vc,
    avg(vc) OVER (
    	PARTITION BY id
    	ORDER BY et
    	ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
) AS avgVC
FROM ws

或者

SELECT 
    id, 
    et, 
    vc,
avg(vc) OVER w AS avgVC,
count(vc) OVER w AS cnt
FROM ws
WINDOW w AS (
    PARTITION BY id
    ORDER BY et
    ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
)
4.特殊语法 —— TOP-N
1.语法
SELECT [column_list]
FROM (
SELECT [column_list],
ROW_NUMBER() OVER ([PARTITION BY col1[, col2...]]
ORDER BY col1 [asc|desc][, col2 [asc|desc]...]) AS rownum
FROM table_name)
WHERE rownum <= N [AND conditions]

ROW_NUMBER() :标识 TopN 排序子句

PARTITION BY col1[, col2…] :标识分区字段,代表按照这个 col 字段作为分区粒度对数据进行排序取 topN,比如下述案例中的 partition by key ,就是根据需求中的搜索关键词(key)做为分区

ORDER BY col1 asc|desc…] :标识 TopN 的排序规则,是按照哪些字段、顺序或逆序进行排序,可以不是时间字段,也可以降序(TopN特殊支持)

WHERE rownum <= N :这个子句是一定需要的,只有加上了这个子句,Flink 才能将其识别为一个TopN 的查询,其中 N 代表 TopN 的条目数

[AND conditions] :其他的限制条件也可以加上

2.案例

取每个传感器最高的3个水位值

select 
    id,
    et,
    vc,
    rownum
from 
(
    select 
        id,
        et,
        vc,
        row_number() over(
            partition by id 
            order by vc desc 
        ) as rownum
    from ws
)
where rownum<=3;
5.特殊语法 —— Deduplication去重

去重,也即上文介绍到的TopN 中 row_number = 1 的场景,但是这里有一点不一样在于其排序字段一定是时间属性列,可以降序,不能是其他非时间属性的普通列。

如果是按照时间属性字段降序,表示取最新一条,会造成不断的更新保存最新的一条。如果是升序,表示取最早的一条,不用去更新,性能更好。

1.语法
SELECT [column_list]
FROM (
SELECT [column_list],
ROW_NUMBER() OVER ([PARTITION BY col1[, col2...]]
ORDER BY time_attr [asc|desc]) AS rownum
FROM table_name)
WHERE rownum = 1
2.案例
select 
    id,
    et,
    vc,
    rownum
from 
(
    select 
        id,
        et,
        vc,
        row_number() over(
            partition by id,vc 
            order by et 
        ) as rownum
    from ws
)
where rownum=1;
6.联结(Join)查询
1.常规联结查询

Inner Join(Inner Equal Join):流任务中,只有两条流 Join 到才输出,输出 +[L, R]

Left Join(Outer Equal Join):流任务中,左流数据到达之后,无论有没有 Join 到右流的数据,都会输出(Join 到输出 +[L, R] ,没 Join 到输出 +[L, null] ),如果右流之后数据到达之后,发现左流之前输出过没有 Join 到的数据,则会发起回撤流,先输出 -[L, null] ,然后输出 +[L, R]

Right Join(Outer Equal Join):有 Left Join 一样,左表和右表的执行逻辑完全相反

Full Join(Outer Equal Join):流任务中,左流或者右流的数据到达之后,无论有没有 Join 到另外一条流的数据,都会输出(对右流来说:Join 到输出 +[L, R] ,没 Join 到输出 +[null, R] ;对左流来说:Join 到输出 +[L, R] ,没 Join 到输出 +[L, null] )。如果一条流的数据到达之后,发现之前另一条流之前输出过没有 Join 到的数据,则会发起回撤流(左流数据到达为例:回撤 -[null, R] ,输出+[L, R] ,右流数据到达为例:回撤 -[L, null] ,输出 +[L, R]

Regular Join 的注意事项:

实时 Regular Join 可以不是 等值 join 。等值 join 和 非等值 join 区别在于, 等值 join数据 shuffle 策略是 Hash,会按照 Join on 中的等值条件作为 id 发往对应的下游; 非等值 join 数据 shuffle 策略是 Global,所有数据发往一个并发,按照非等值条件进行关联

流的上游是无限的数据,所以要做到关联的话,Flink 会将两条流的所有数据都存储在 State 中,所以 Flink 任务的 State 会无限增大,因此你需要为 State 配置合适的 TTL,以防止 State 过大。

案例:



![img](https://img-blog.csdnimg.cn/img_convert/c0f52b286d30be9045256e1c34bc48af.png)
![img](https://img-blog.csdnimg.cn/img_convert/ba19249c4569b04969775885ac8155cd.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

照行数聚合,第二种为按照时间区间聚合


###### 2.案例


**1.按照时间区间聚合**


统计每个传感器前10秒到现在收到的水位数据条数。



SELECT
id,
et,
vc,
count(vc) OVER (
PARTITION BY id
ORDER BY et
RANGE BETWEEN INTERVAL ‘10’ SECOND PRECEDING AND CURRENT ROW
) AS cnt
FROM ws


或者



SELECT
id,
et,
vc,
count(vc) OVER w AS cnt,
sum(vc) OVER w AS sumVC
FROM ws
WINDOW w AS (
PARTITION BY id
ORDER BY et
RANGE BETWEEN INTERVAL ‘10’ SECOND PRECEDING AND CURRENT ROW
)


**2.按照行数聚合**


统计每个传感器前5条到现在数据的平均水位



SELECT
id,
et,
vc,
avg(vc) OVER (
PARTITION BY id
ORDER BY et
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
) AS avgVC
FROM ws


或者



SELECT
id,
et,
vc,
avg(vc) OVER w AS avgVC,
count(vc) OVER w AS cnt
FROM ws
WINDOW w AS (
PARTITION BY id
ORDER BY et
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
)


##### 4.特殊语法 —— TOP-N


###### 1.语法



SELECT [column_list]
FROM (
SELECT [column_list],
ROW_NUMBER() OVER ([PARTITION BY col1[, col2…]]
ORDER BY col1 [asc|desc][, col2 [asc|desc]…]) AS rownum
FROM table_name)
WHERE rownum <= N [AND conditions]


ROW\_NUMBER() :标识 TopN 排序子句


PARTITION BY col1[, col2...] :标识分区字段,代表按照这个 col 字段作为分区粒度对数据进行排序取 topN,比如下述案例中的 partition by key ,就是根据需求中的搜索关键词(key)做为分区


ORDER BY col1 [asc|desc](#)...] :标识 TopN 的排序规则,是按照哪些字段、顺序或逆序进行排序,可以不是时间字段,也可以降序(TopN特殊支持)


WHERE rownum <= N :这个子句是一定需要的,只有加上了这个子句,Flink 才能将其识别为一个TopN 的查询,其中 N 代表 TopN 的条目数


[AND conditions] :其他的限制条件也可以加上


###### 2.案例


取每个传感器最高的3个水位值



select
id,
et,
vc,
rownum
from
(
select
id,
et,
vc,
row_number() over(
partition by id
order by vc desc
) as rownum
from ws
)
where rownum<=3;


##### 5.特殊语法 —— Deduplication去重


去重,也即上文介绍到的TopN 中 row\_number = 1 的场景,但是这里有一点不一样在于其排序字段一定是时间属性列,可以降序,不能是其他非时间属性的普通列。


如果是按照时间属性字段降序,表示取最新一条,会造成不断的更新保存最新的一条。如果是升序,表示取最早的一条,不用去更新,性能更好。


###### 1.语法



SELECT [column_list]
FROM (
SELECT [column_list],
ROW_NUMBER() OVER ([PARTITION BY col1[, col2…]]
ORDER BY time_attr [asc|desc]) AS rownum
FROM table_name)
WHERE rownum = 1


###### 2.案例



select
id,
et,
vc,
rownum
from
(
select
id,
et,
vc,
row_number() over(
partition by id,vc
order by et
) as rownum
from ws
)
where rownum=1;


##### 6.联结(Join)查询


###### 1.常规联结查询


Inner Join(Inner Equal Join):流任务中,只有两条流 Join 到才输出,输出 +[L, R]


Left Join(Outer Equal Join):流任务中,左流数据到达之后,无论有没有 Join 到右流的数据,都会输出(Join 到输出 +[L, R] ,没 Join 到输出 +[L, null] ),如果右流之后数据到达之后,发现左流之前输出过没有 Join 到的数据,则会发起回撤流,先输出 -[L, null] ,然后输出 +[L, R]


Right Join(Outer Equal Join):有 Left Join 一样,左表和右表的执行逻辑完全相反


Full Join(Outer Equal Join):流任务中,左流或者右流的数据到达之后,无论有没有 Join 到另外一条流的数据,都会输出(对右流来说:Join 到输出 +[L, R] ,没 Join 到输出 +[null, R] ;对左流来说:Join 到输出 +[L, R] ,没 Join 到输出 +[L, null] )。如果一条流的数据到达之后,发现之前另一条流之前输出过没有 Join 到的数据,则会发起回撤流(左流数据到达为例:回撤 -[null, R] ,输出+[L, R] ,右流数据到达为例:回撤 -[L, null] ,输出 +[L, R]


Regular Join 的注意事项:


实时 Regular Join 可以不是 等值 join 。等值 join 和 非等值 join 区别在于, 等值 join数据 shuffle 策略是 Hash,会按照 Join on 中的等值条件作为 id 发往对应的下游; 非等值 join 数据 shuffle 策略是 Global,所有数据发往一个并发,按照非等值条件进行关联


流的上游是无限的数据,所以要做到关联的话,Flink 会将两条流的所有数据都存储在 State 中,所以 Flink 任务的 State 会无限增大,因此你需要为 State 配置合适的 TTL,以防止 State 过大。


案例:



[外链图片转存中…(img-g2OBt5F5-1715089918335)]
[外链图片转存中…(img-7tuqMX7c-1715089918335)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值