Flink -- 批流一体

Flink底层原理

作为大数据实时计算中不可或缺的一部分 flink是大数据实时处理非常重要的一部分 也对刚刚接触大数据学习的同学们是很难去理解的一部分 所以首先用一个小小的例子让大家对Flink有初步的了解

以WordCount为例,执行流程如下:
1.创建socket nc -lk 8888 实时读取数据(socket只能被一个线程连接)
  DataStream<String> lines = env.socketTextStream("master", 8888);
​
2.DataStream创建task,从master中读取数据,任务数为1 -- taskA
​
3.flatmap取出数据 ,前面设置并行度为二(env.setParallelism(2);) ,所以此时task数量为2
​
taskA --> task task 的过程称为轮循 -- 避免数据倾斜,避免某个数据节点数据变多 (上游task1将task循环发送给下游task)
​
以(a,1)为例,当第一个task接收第一个a,传入第二个a时,会轮循到第二个task执行
​
得到新的DataStream
​
4.map将数据转成(k,v)格式,创建新的DataStream,此时过程为("窄依赖"),没有shuffle过程此时两个过程的task一一对应,在Flink中看成一个task
​
5.keyby。创建 KeyedStream(shuffle产生),将同一个key发送(spark中是拉取)到同一个key中.
​
此过程如何判断key被分入哪个分区?? -- hash分区 key.hashCode() % 并行度 取余(计算传入key的hash值 % 2 = 1 )
​
6.对分区后的value进行累加计算 创建新的DataStream,最后print输出结果

执行代码:
package com.shujia.yj;
​
​
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
​
​
public class Code01WordCount {
    public static void main(String[] args) throws Exception {
        //1、创建flink执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
        /**
         * 多态:父类的引用指向子类的对象       java中 引用 = 对象
         * flatMap方法用于处理数据,每一条数据执行一次
         * @param line: 一行数据
         * @param out:用于将数据发生到下游,可以发生多条
         */
         
        env.setParallelism(2); //任务并行数
​
        env.setBufferTimeout(200);//毫秒
        
        //2、读取数据,构建DataStream
        
        //启动socket nc -lk 8888
        DataStream<String> lines = env.socketTextStream("master", 8888); --流处理
        
        //3.统计单词数量
        // -- 一行转多行
        SingleOutputStreamOperator<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
            @Override
            public void flatMap(String line, Collector<String> out) throws Exception {//实现
                String[] split = line.split(",");
                for (String word : split) {
                    out.collect(word);
                }
            }
        });
​
        /**
         * map每一条数据执行一次
         * @param word:一行数据
         * @return 新的数据
         */
        //转换成kv格式
        SingleOutputStreamOperator<Tuple2<String, Integer>> kvDataStream = words.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String word) {
                return Tuple2.of(word, 1);
            }
        });
​
​
        //按照单词分组
        //kv -> kv.f0  lambda表达式
        KeyedStream<Tuple2<String, Integer>, String>  keyByDataStream = kvDataStream.keyBy(kv -> kv.f0);
​
        SingleOutputStreamOperator<Tuple2<String, Integer>> sum = keyByDataStream.sum(1);
​
        sum.print();
        //启动flink
        env.execute();
​
/**
 *
 * 相同的key会进到同一个task中,但是同一个task中不会只对应一个key
 *例 : 做wordcount时,出现    1000个单词,但是电脑的线程只有16核,这样一个task中就会出现多个key
 */
​
​
    }
}
flink处理数据的两种方式
1、同一个代码既能用于批处理,也能用于流处理(计算引擎的流批统一)
2、flink可以基于文件做流处理也能做批处理(数据源的流批统一)
批数据和流数据  --  有界数据流和无界数据流 ? 是否对应
​
​
1.流处理数据,以socket为例
​
DataStream<String> lines = env.socketTextStream("master", 8888); --流处理
        
​
2.批处理数据,需要修改flink默认的处理模式
​
env.setRuntimeMode(RuntimeExecutionMode.BATCH); 以batch为例
​
DataStream<String> word = env.readTextFile("data/word.txt")
        
                BATCH: 批处理
                 1、输出最终结果
                 2、只能用于处理有界流,不能用于处理无界流
                 3、批处理模式底层是mapreduce模型
​
                 STREAMING: 流处理模式
                 1、输出连续结果,每一条数据都会输出一个结果
                 2、可以处理有界流,也可以处理无界流
                 3、底层是持续流模型     
               

1.Flink中没有shuffle过程的task会自动合并
​
2.上游task和下游task同时间执行(出现shuffle过程才会把task分为上游task和下游task)
​
3.数据从上游发送到下游的方式
    -- 默认是每32kb或者200毫秒一次  -- 一次发送一批数据效率更高
​
 设置数据从上游发送到下游的时间:env.setBufferTimeout(200);
​
4.spark中先执行map端再执行reduce端,且数据在map端村存在预聚合过程,Flink中不存在预聚合,因为数据是流动的,如果对数据进行预聚合,会浪费时间
​
5.Flink底层为持续流模型(Dataflow),上游task和下游task同时执行启动,默认不会做预聚合,为了降低数据处理的延迟
​
6.如果使用Flink做批处理,那么他的底层原理也是mapReduce
​
7.流数据:代码执行完.相当于创建了管道,数据在管道中,随时可以调用。
​
8.Flink中的计算都是有状态计算

reduce:聚合函数
 spark中的算子都是懒执行算子,需要action触发,而Flink中算子不需要action也会触发,比如下面的情况中,不需要print,输入数据,也会执行一次。
 
 
 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
        env.setParallelism(10);
​
        DataStreamSource<String> lines = env.socketTextStream("master", 8888);
​
        SingleOutputStreamOperator<KeyValue> map = lines.map(word -> new KeyValue(word, 1));
​
        KeyedStream<KeyValue, String> keyValueStringKeyedStream = map.keyBy(keyValue -> keyValue.getWord());
​
        SingleOutputStreamOperator<KeyValue> reduce = keyValueStringKeyedStream.reduce(new ReduceFunction<KeyValue>() {
            @Override
            public KeyValue reduce(KeyValue kv1, KeyValue kv2) throws Exception {
                System.out.println("kv1:" + kv1);
                System.out.println("kv2:" + kv2);
                String word = kv1.getWord();
                int num = kv1.getNum() + kv2.getNum();
                return new KeyValue(word, num);
            }
        });
​
        reduce.print();
​
        env.execute();
        
     3> KeyValue(word=yujie, num=1)  
     kv1:KeyValue(word=yujie, num=1)   -- kv1中存储的是之前计算的结果,也就是状态(第一条数据除外)
     kv2:KeyValue(word=yujie, num=1)   -- kv2中存储的是新传入的数据
     3> KeyValue(word=yujie, num=2)    -- 对kv1和kv2中做累加计算(自定义)
     kv1:KeyValue(word=yujie, num=2)
     kv2:KeyValue(word=yujie, num=1)
     3> KeyValue(word=yujie, num=3)
     kv1:KeyValue(word=yujie, num=3)
     kv2:KeyValue(word=yujie, num=1)
     3> KeyValue(word=yujie, num=4)
     5> KeyValue(word=wuyxi, num=1)

Flink提交任务的步骤是需要先构建Dataflow图,即代码的关系图吗,算子之间,DataStream之间的关系图
​
出现shuffle切分,并行度发生改变切分
​
合并切分之后的task最终会在taskSlot即task槽中运行
​
共享资源槽:在同一个task槽中,运行上游和下游的所有task共享同一个资源 -- 一个task一个资源(资源浪费)
​
一个并行度一个资源,Flink任务资源需求的多少和task无关,之后任务的并行度有关

Apache Flink架构

Flink on YARN三种模式

1、per job mode

1、类似spark on yarn 的client模式 2、如果任务执行出错,在本地可以看到部分错误信息 3、在本地执行main函数,构建dataFlow图,在将dataflow提交到JobManager中运行 4、每一个flink任务单独申请资源,启动一个jobManager和多个taskmanager,任务之间互不影响

执行流程:
1.在客户端提交任务:flink run -t yarn-per-job -c com.shujia.flink.core.Demo4Submit flink-1.0.jar
​
 -- 资源调度
​
2.向resourcemanger申请资源,申请启动jobManger
​
3.RM随机分配一个节点启动jobManger
​
4.在本地客户端构建DataFlow图,将图提交到jobManger中
​
 --任务调度
 
5.jobmanger根据任务并行度申请TaskManger
​
6.rm分配一批节点启动TaskManger
​
7.jobmanger将任务发送到TaskManger中执行
​
--spark和Flink都是粗粒度资源调度(在任务提交之前,将任务资源全部申请下来,这样会节约了任务开始执行时资源申请的时间,但是会浪费资源  -- 和mapreduce(细资源调度)比较)

2、appplication mode

1、类似spark on yarn 的cluster模式 2、在本地看不到错误信息 3、main函数在JobManager中执行,本地只负责提交任务 4、每一个flink任务单独申请资源,启动一个jobManager和多个taskmanager,任务之间互不影响

用于生产环境

# 获取任务日志方式
yarn logs -applicationId application_1691044931290_0008
执行流程
1.在客户端提交任务:flink run-application -t yarn-application -c com.shujia.yj.core.Core.Code01WordCount FlinkCode-1.0-SNAPSHOT.jar 
​
 -- 资源调度
​
2.向resourcemanger申请资源,申请启动jobManger
​
3.RM随机分配一个节点启动jobManger
  
4.在jobmanger中构建Dataflow图,根据并行度申请资源 -- 本地提交任务之后,不再参与任务流程
​
5.rm分配一批节点启动TsakMager
​
6.jm将task发送到taskmanger中执行

3、session mode

1、先在yarn中申请资源启动一个JobManager,再提交任务 2、提交的任务公用一个JobManager,动态申请taskManager,任务取消,taskManager会自动回收

一般用于测试环境

#1、启动session集群
yarn-session.sh -d
​
​
#2、提交任务
flink run -t yarn-session -p 2 -Dyarn.application.id=application_1691044931290_0009  -c com.shujia.flink.core.Demo4Submit flink-1.0.jar
​
​
-P 2 :设置并行度为2
执行流程
  -- Flink集群的启动和任务的提交分开执行
      可否看成 资源调度和任务调度分开执行
资源调度:
    1.在yarn中启动jobManger -- 客户端中
​
    2.向resourceManger申请资源启动JobManger、
​
    3.rm随机分配节点启动jm
​
任务提交:
    1.再次提交任务
    
    2.在本地构建daataflow图,提交给jm
    
    3.jm根据任务并行度向rm申请TaskManger
    
    4.rm随机分配一批节点启动tm
    
    5.jm将task发送到taskmanger中执行
​
任何的大数据计算引擎都大致分为两个步骤:资源调度+任务调度

时间窗口

事件时间:数据发生的时间
​
处理时间:现实中的时间
​
​
         * 1、SlidingEventTimeWindows:滑动的事件时间窗口
         * 2、SlidingProcessingTimeWindows:滑动的处理时间窗口
         * 3、TumblingEventTimeWindows:滚动的事件时间窗口
         * 4、TumblingProcessingTimeWindows:滚动的处理时间窗口
​
​
处理时间
TumblingProcessingTimeWindows.of( Time.seconds(10)):每隔十秒统计数据
​
​
处理时间的两种方式:
​
1 .<Tuple2<String, Long>>forMonotonousTimestamps()  -- 单调递增时间戳分配器  ,数据不一定是按照顺序执行,会有一定概率丢失数据
​
2.最大固定延迟的时间戳分配器 -- 也就是把水位线前移
.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(2))
​
​
简单事件窗口:
 -- 时间以时间发生的时间为准,和现实中的时间(处理时间)没有关系
例:
ass
//转换成kv格式
.map(event -> Tuple2.of(event.f0, 1), Types.TUPLE(Types.STRING, Types.INT))
//按照单词分组
.keyBy(kv -> kv.f0)
//每个5秒一个窗口
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//统计单词的数量
.sum(1)
.print();
​
​
数据输入:
java,1691114101000
java,1691114102000
java,1691114105000
java,1691114106000         -- 窗口的计数为左闭右开
java,1691114107000
​
结果:
(java,2) java,1691114101000 java,1691114101000
​
​
例二:TumblingEventTimeWindows. + 设置水位线前移 
​
 WatermarkStrategy<Tuple2<Integer, Long>> watermarkStrategy = WatermarkStrategy
                //java,1691114102000m,如果数据乱序可能还会丢失数据
                //设置水位线生成策略,水位线等于最新一条数据的时间戳,水位线只能增加
                //.<Tuple2<String, Long>>forMonotonousTimestamps()
​
                //数据之间存在最大固定延迟的时间戳分配器
                //水位线前移时间
                .<Tuple2<Integer, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                //指定时间字段
                .withTimestampAssigner((event, ts) -> event.f1);
                
                 + 
                 
                 
  ass
                //转换成kv格式
                .map(event -> Tuple2.of(event.f0, 1), Types.TUPLE(Types.STRING, Types.INT))
                //安装单词分组
                .keyBy(kv -> kv.f0)
                //每个5秒一个窗口
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                //统计单词的数量
                .sum(1)
                .print();
输入:          
java,1691114101000
java,1691114105000     
java,1691114103000      
java,1691114108000        0---5(7)
java,1691114109000            5---10(12)
java,1691114110000                10---15(17)
java,1691114115000
java,1691114116000
java,1691114120000
​
-- 每隔五秒一个窗口,且水位线前移2秒:实际中窗口可以看成0--7,但是实际技术还是以0--5中为准  
例如:第一秒开始,第二个5秒不停,第三个3秒,遇到第四个八秒停止,开始分组聚合,计算0--5中的数据(不包括5) = 2
​
​
结果:
(java,2) java,1691114101000  java,1691114103000
(java,3) java,1691114105000  java,1691114108000 java,1691114109000
(java,1) java,1691114110000
     
     
     
3.滑动事件窗口:
//每5秒统计最近10秒的数据
                .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
​
java,1691114101000
java,1691114105000     
java,1691114103000      
java,1691114108000        
java,1691114109000            
java,1691114110000                
java,1691114115000
java,1691114116000
java,1691114120000
​
结果:
​
(java,2)
(java,5)
(java,4)
​
 95----5  java,1691114101000 +  java,1691114103000
     0----10  java,1691114101000 +  java,1691114103000  + java,1691114105000 + java,1691114108000 + java,1691114109000
        5----15 java,1691114105000 +  java,1691114108000  +  java,1691114109000 +  java,1691114110000
           10----20  ?为什么不统计10---20的数据 ,因为还需要接下来的五秒发生才会统计上10秒的数据
            --每5秒统计最近10秒的数据

kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic words
​
​
9> +I[words, 1]
9> -U[words, 1]
9> +U[words, 2]

Flink状态与容错

状态开始存储在task的内存中,后会被持久化到磁盘中(HDFS中) (为什么本身存在内存中,还要持久化到磁盘中?   因为一旦task宕机或者服务器断电,数据就不再存在。) -- 持久化的过程被称为checkpoint
​
状态会被checkpoint"定时"持久化到HDFS中去
​
​
  //指定保存checkpoint路径
        env.getCheckpointConfig().setCheckpointStorage(new Path("hdfs://master:9000/flink/checkpoint"));
​
        //任务被手动取消时保留最新的一次checkpoint
        env.getCheckpointConfig().setExternalizedCheckpointCleanup(
                CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

flink sql

窗口函数

1.滚动窗口 -- 时间时间

什么是滚动窗口 ? -- 就是将数据按照规格划分成为一个个的窗口 ,方便计算

创建:

-- 创建表
CREATE TABLE bid (
    bidtime TIMESTAMP(3),
    price  DECIMAL(10, 2) ,
    item  STRING,
    WATERMARK FOR bidtime AS bidtime -- 指定事件时间和水位线
) WITH (
    'connector' = 'kafka',
    'topic' = 'bid',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true', -- 跳过脏数据
    'csv.field-delimiter' = '|' -- 指定字段分隔符
);
​
​
-- 生成数据
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic bid
2020-04-15 08:05:00|4.00|C
2020-04-15 08:07:00|2.00|A
2020-04-15 08:09:00|5.00|D
2020-04-15 08:11:00|3.00|B
​
​
-- TUMBLE: 滚动窗口函数,
-- 在源表的基础上增加窗口开始时间,窗口结束时间和窗口时间
SELECT bidtime,price,item,window_start,window_end,window_time 
FROM 
TABLE(
   TUMBLE(TABLE bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)
);
​
​
bidtime                      price     item      window_start              window_end             window_time
2020-04-15 08:05:00.000      4.00      C  2020-04-15 08:00:00.000 2020-04-15 08:10:00.000 2020-04-15 08:09:59.999
2020-04-15 08:09:00.000      5.00      D 2020-04-15 08:00:00.000 2020-04-15 08:10:00.000 2020-04-15 08:09:59.999
2020-04-15 08:11:00.000      3.00      B 2020-04-15 08:10:00.000 2020-04-15 08:20:00.000 2020-04-15 08:19:59.999
​
​
也正如上面所示,就是划分了窗口方便了后续的计算
​
-- 在窗口函数之后进行窗口聚合
-- 每隔10分钟计算所有商品的平均价格
-- 当水位线大于等于等于窗口的结束时间才会触发窗口的计算
​
SELECT 
    window_start,
    window_end,
    avg(price) as avg_price
FROM 
TABLE(
   TUMBLE(TABLE bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)
)
group by 
    window_start,
    window_end;
    
    
    
输入:
>2020-04-15 08:07:00|2.00|A
>2020-04-15 08:11:00|3.00|B
>2020-04-15 08:09:00|5.00|D
>2020-04-15 08:13:00|1.00|E
>2020-04-15 08:21:00|6.00|G
​
​
输出:
  window_start                window_end                                            avg_price
 2020-04-15 08:00:00.000     2020-04-15 08:10:00.000                                 2.000000
 2020-04-15 08:10:00.000     2020-04-15 08:20:00.000                                 2.000000

2、滑动窗口 -- 处理时间

每隔n秒,计算m秒内的数据

-- 创建表
-- PROCTIME()获取处理时间的函数
CREATE TABLE bid_procime (
    price  DECIMAL(10, 2) ,
    item  STRING,
    ts AS PROCTIME() -- 声明一个额外的列作为处理时间属性
) WITH (
    'connector' = 'kafka',
    'topic' = 'bid',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup', 
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true', -- 跳过脏数据
    'csv.field-delimiter' = '|' -- 指定字段分隔符
);
​
​
-- 生成数据
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic bid
4.00|C
2.00|A
5.00|D
3.00|B
1.00|E
6.00|F
​
​
-- 滑动窗口
SELECT ts,price,item,window_start,window_end,window_time 
FROM 
TABLE(
   HOP(TABLE bid_procime, DESCRIPTOR(ts),INTERVAL '5' SECOND, INTERVAL '15' SECOND)
);
​
-- 每隔5秒计算最近15秒所有商品的平均价格
SELECT 
window_start,window_end,avg(price) as avg_price
FROM 
TABLE(
   HOP(TABLE bid_procime, DESCRIPTOR(ts),INTERVAL '5' SECOND, INTERVAL '15' SECOND)
)
group by 
window_start,window_end;
​
​
输出:
  window_start              window_end                                           avg_price
 2023-08-13 21:48:05.000 2023-08-13 21:48:20.000                                 4.000000
 2023-08-13 21:48:10.000 2023-08-13 21:48:25.000                                 4.000000
 2023-08-13 21:48:15.000 2023-08-13 21:48:30.000                                 3.250000
 2023-08-13 21:48:20.000 2023-08-13 21:48:35.000                                 3.000000
 2023-08-13 21:48:25.000 2023-08-13 21:48:40.000                                 1.000000
​
​
3、会话窗口

一段时间没有数据开始计算

​
-- 一段时间没有数据开始计算
select 
    item,
    SESSION_START(ts,INTERVAL '10' SECOND) as win_start,
    SESSION_END(ts,INTERVAL '10' SECOND) as win_end,
    count(1) as num
from 
    bid_procime
group by 
    item,
    SESSION(ts , INTERVAL '10' SECOND);

窗口聚合

-- 实时统计每个商品最近1小时的总的交易金额,在每一条数据后面增加总的订单金额
1.RANGE BETWEEN INTERVAL '60' MINUTE PRECEDING AND CURRENT ROW : 从一个小时之前到当前行
​
​
-- 实时统计每个商品最近5条数据的总的交易金额,在每一条数据后面增加总的订单金额
2.ROWS BETWEEN 4 PRECEDING AND CURRENT ROW 从前吗第四条到当前行
select 
    order_id,
    order_time,
    amount,
    product,
    sum(amount) over(
        partition by product 
        order by order_time
        RANGE BETWEEN INTERVAL '60' MINUTE PRECEDING AND CURRENT ROW
    ) as sum_amount
from 
    orders
    
    
    
​

排序 -- 注意点!

在Flink的计算中,排序的方式只有两种:

1.按照时间字段进行排序 -- 时间字段本身就是升序了,不需要计算

2.对数据进行row_number时不能进行order-by,只能取排名 -- 取top之后flink需要维护的数据变少,计算代价不会一直增大

-- 在流处理中over聚合必须安装时间字段进行升序排序,
​
1.如果使用普通字段进行排序,没来一条数据都需要重新计算新的顺序,计算代价太大  因为是流处理
2.批处理模式没有限制
3.如果安装普通字段排序,每来一条新的数据,都需要重新计算顺序,计算代价太大,状态也会越来越大
select 
    order_id,
    order_time,
    amount,
    product,
    sum(amount) over(
        partition by product 
        order by amount -- 不能安装普通字段排序
    ) as sum_amount
from 
    orders;

关联

注意流处理中的关联和普通批处理中的关联的区别,数据是源源不断的产生的 。

1、Regular Joins

-- 在流出模式中使用常规的关联方式,flink会将两个表的数据一直保持在状态中,状态会越来越大 -- 可以设置状态过期时间,一段时间清理状态,比如值保留最近一天的状态

set 'table.exec.state.ttl' = '5000';

-- kafka source 
CREATE TABLE students_kafka (
    id STRING,
    name STRING,
    age INT,
    gender STRING,
    clazz STRING
) WITH (
    'connector' = 'kafka',
    'topic' = 'students',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true' -- 跳过脏数据
);
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic students
1500100001,施笑槐,22,女,文科六班
1500100002,吕金鹏,24,男,文科六班
1500100003,单乐蕊,22,女,理科六班
1500100004,葛德曜,24,男,理科三班
1500100005,宣谷芹,22,女,理科五班
​
-- kafka source
CREATE TABLE scores_kafka (
    sid STRING,
    cid STRING,
    score DOUBLE
) WITH (
    'connector' = 'kafka',
    'topic' = 'scores',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true' -- 跳过脏数据
);
​
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic scores
1500100001,1000001,98
1500100001,1000002,5
1500100001,1000003,137
1500100001,1000004,29
1500100001,1000005,85
1500100001,1000006,52
​
​
​
​
select a.id,a.name,b.score from 
students_kafka as a
full join 
scores_kafka as b
on a.id=b.sid;
2、Interval Joins

只关联指定时间内的数据

​
-- 只关联一段时间内的数据,不需要将两个表的数据一直保存在状态中,状态就不会越来越大
​
select a.id,b.score,a.ts,b.ts from 
students_kafka_event_time as a, scores_kafka_event_time as b
where 
a.id=b.sid
and a.ts BETWEEN b.ts - INTERVAL '5' SECOND AND b.ts;
3、Temporal Joins Lookup Join !!

时态表 mysql hbase redis 将维度表数据存入上面的数据库中

Mysql -- 单机存储  -- 可以使用sql查询,但是存储的数据量少
hbase -- 分布式存储 ,可以存储大量数据,但是不能使用sql查询,只能借助工具进行查询语句的执行
维度表 -- 用户表,商品表... 有主键,一个维度对应一条数据
事实表 -- 数据不固定 ,流动存在
时态表 -- 时态表(Temporal Table)是一张随时间变化的表 – 在 Flink 中称为动态表
​
维表的数据存在数据库中,而事实表的数据存在kafka中
如何使用?
-- FOR SYSTEM_TIME AS OF a.order_time: 使用订单表的时间到汇率表中查询对应时间的数据
 使用a表的时间到b表中查找
​
例1:使用订单表(维度表)的时间到汇率表(事实表)中查询对应时间的数据
​
​
-- 订单表
CREATE TABLE orders (
    order_id    STRING,
    price       DECIMAL(32,2),
    currency    STRING,
    order_time  TIMESTAMP(3),
    WATERMARK FOR order_time AS order_time
) WITH (
    'connector' = 'kafka',
    'topic' = 'orders',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true' -- 跳过脏数据
);
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic orders
o_001,100,EUR,2023-08-10 17:10:05
o_002,200,EUR,2023-08-10 17:10:15
o_003,300,EUR,2023-08-10 17:10:20
​
-- 汇率表
CREATE TABLE currency_rates (
    currency STRING,
    conversion_rate DECIMAL(32, 2),
    update_time TIMESTAMP(3) ,
    WATERMARK FOR update_time AS update_time,
    PRIMARY KEY(currency) NOT ENFORCED
) WITH (
    'connector' = 'kafka',
    'topic' = 'currency_rates',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'earliest-offset', -- earliest-offset  latest-offset
    'format' = 'canal-json' 
);
​
​
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic currency_rates
insert into currency_rates values
('EUR',10,TIMESTAMP'2023-08-10 17:10:05'),
('EUR',12,TIMESTAMP'2023-08-10 17:10:12'),
('EUR',9,TIMESTAMP'2023-08-10 17:10:22');
​
-- FOR SYSTEM_TIME AS OF a.order_time: 使用订单表的时间到汇率表中查询对应时间的数据
​
select a.order_id,a.price,b.conversion_rate,b.update_time from 
orders as a
left join 
currency_rates  FOR SYSTEM_TIME AS OF a.order_time as b
on a.currency=b.currency;
例二 - - lookup join

-- 使用常规关联方式实现维表关联 1、状态会越来越大 2、flink读取jdbc只会在任务启动的时候读取一次。如果数据库数据更新了,flink不能发现

所以使用lookup join

FOR SYSTEM_TIME AS OF a.proctime: 当流表每来一条数据会去数据库中查询维表数据

策略:

增加缓存

当需要从数据库中取数据时,先从缓存中取数据 -- 因为从数据库中查数据需要大量的时间(20ms),而从缓存中读取数据的时间可以忽略不记

    'lookup.cache.max-rows' = '2000', -- 最大缓存的数据量
    'lookup.cache.ttl' ='10000' -- 缓存过期时间

​
-- 订单表
CREATE TABLE orders (
    order_id    STRING,
    price       DECIMAL(32,2),
    user_id    STRING,
    order_time  TIMESTAMP(3),
    proctime as PROCTIME() -- 处理时间
) WITH (
    'connector' = 'kafka',
    'topic' = 'orders',
    'properties.bootstrap.servers' = 'master:9092,node1:9092,node2:9092',
    'properties.group.id' = 'testGroup',
    'scan.startup.mode' = 'latest-offset', -- earliest-offset  latest-offset
    'format' = 'csv', -- 指定数据的格式,csv格式需要保证字段的顺序和数据的顺序一致
    'csv.ignore-parse-errors' ='true' -- 跳过脏数据
);
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic orders
o_001,100,user001,2023-08-10 17:10:10
o_001,100,user001,2023-08-10 17:10:10
o_002,200,user002,2023-08-10 17:10:15
o_003,300,user003,2023-08-10 17:10:20
o_004,300,user004,2023-08-10 17:10:20
​
-- 用户表
CREATE TABLE users (
    user_id STRING,
    user_name STRING,
    age INT,
    gender STRING,
    PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
    'connector' = 'jdbc', 
    'url' = 'jdbc:mysql://master:3306/student',
    'table-name' = 'users', -- 需要自己去数据库中创建,插入数据
    'username' ='root',
    'password' ='123456',
    'lookup.cache.max-rows' = '2000', -- 最大缓存的数据量
    'lookup.cache.ttl' ='10000' -- 缓存过期时间
);
​
-- 使用常规关联方式实现维表关联
-- 1、状态会越来越大
-- 2、flink读取jdbc只会在任务启动的时候读取一次。如果数据库数据更新了,flink不能发现
select a.order_id,a.price,b.user_name,b.age from 
orders as a
left join 
users as b
on a.user_id=b.user_id;
​
​
--- lookup join 
--  FOR SYSTEM_TIME AS OF a.proctime: 当流表每来一条数据会去数据库中查询维表数据
select a.order_id,a.price,b.user_name,b.age from 
orders as a
left join 
users  FOR SYSTEM_TIME AS OF a.proctime as b
on a.user_id=b.user_id;
​

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值