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;